1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.backup;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.content.res.XmlResourceParser;
22 import android.os.ParcelFileDescriptor;
23 import android.os.Process;
24 import android.os.storage.StorageManager;
25 import android.os.storage.StorageVolume;
26 import android.system.ErrnoException;
27 import android.system.Os;
28 import android.text.TextUtils;
29 import android.util.ArrayMap;
30 import android.util.ArraySet;
31 import android.util.Log;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.File;
39 import java.io.FileInputStream;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.util.Map;
43 import java.util.Set;
44 
45 /**
46  * Global constant definitions et cetera related to the full-backup-to-fd
47  * binary format.  Nothing in this namespace is part of any API; it's all
48  * hidden details of the current implementation gathered into one location.
49  *
50  * @hide
51  */
52 public class FullBackup {
53     static final String TAG = "FullBackup";
54     /** Enable this log tag to get verbose information while parsing the client xml. */
55     static final String TAG_XML_PARSER = "BackupXmlParserLogging";
56 
57     public static final String APK_TREE_TOKEN = "a";
58     public static final String OBB_TREE_TOKEN = "obb";
59     public static final String KEY_VALUE_DATA_TOKEN = "k";
60 
61     public static final String ROOT_TREE_TOKEN = "r";
62     public static final String FILES_TREE_TOKEN = "f";
63     public static final String NO_BACKUP_TREE_TOKEN = "nb";
64     public static final String DATABASE_TREE_TOKEN = "db";
65     public static final String SHAREDPREFS_TREE_TOKEN = "sp";
66     public static final String CACHE_TREE_TOKEN = "c";
67 
68     public static final String DEVICE_ROOT_TREE_TOKEN = "d_r";
69     public static final String DEVICE_FILES_TREE_TOKEN = "d_f";
70     public static final String DEVICE_NO_BACKUP_TREE_TOKEN = "d_nb";
71     public static final String DEVICE_DATABASE_TREE_TOKEN = "d_db";
72     public static final String DEVICE_SHAREDPREFS_TREE_TOKEN = "d_sp";
73     public static final String DEVICE_CACHE_TREE_TOKEN = "d_c";
74 
75     public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";
76     public static final String SHARED_STORAGE_TOKEN = "shared";
77 
78     public static final String APPS_PREFIX = "apps/";
79     public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/";
80 
81     public static final String FULL_BACKUP_INTENT_ACTION = "fullback";
82     public static final String FULL_RESTORE_INTENT_ACTION = "fullrest";
83     public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken";
84 
85     public static final String FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION = "clientSideEncryption";
86     public static final String FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER = "deviceToDeviceTransfer";
87     public static final String FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION =
88             "fakeClientSideEncryption";
89 
90     /**
91      * @hide
92      */
backupToTar(String packageName, String domain, String linkdomain, String rootpath, String path, FullBackupDataOutput output)93     static public native int backupToTar(String packageName, String domain,
94             String linkdomain, String rootpath, String path, FullBackupDataOutput output);
95 
96     private static final Map<String, BackupScheme> kPackageBackupSchemeMap =
97             new ArrayMap<String, BackupScheme>();
98 
getBackupScheme(Context context)99     static synchronized BackupScheme getBackupScheme(Context context) {
100         BackupScheme backupSchemeForPackage =
101                 kPackageBackupSchemeMap.get(context.getPackageName());
102         if (backupSchemeForPackage == null) {
103             backupSchemeForPackage = new BackupScheme(context);
104             kPackageBackupSchemeMap.put(context.getPackageName(), backupSchemeForPackage);
105         }
106         return backupSchemeForPackage;
107     }
108 
getBackupSchemeForTest(Context context)109     public static BackupScheme getBackupSchemeForTest(Context context) {
110         BackupScheme testing = new BackupScheme(context);
111         testing.mExcludes = new ArraySet();
112         testing.mIncludes = new ArrayMap();
113         return testing;
114     }
115 
116 
117     /**
118      * Copy data from a socket to the given File location on permanent storage.  The
119      * modification time and access mode of the resulting file will be set if desired,
120      * although group/all rwx modes will be stripped: the restored file will not be
121      * accessible from outside the target application even if the original file was.
122      * If the {@code type} parameter indicates that the result should be a directory,
123      * the socket parameter may be {@code null}; even if it is valid, no data will be
124      * read from it in this case.
125      * <p>
126      * If the {@code mode} argument is negative, then the resulting output file will not
127      * have its access mode or last modification time reset as part of this operation.
128      *
129      * @param data Socket supplying the data to be copied to the output file.  If the
130      *    output is a directory, this may be {@code null}.
131      * @param size Number of bytes of data to copy from the socket to the file.  At least
132      *    this much data must be available through the {@code data} parameter.
133      * @param type Must be either {@link BackupAgent#TYPE_FILE} for ordinary file data
134      *    or {@link BackupAgent#TYPE_DIRECTORY} for a directory.
135      * @param mode Unix-style file mode (as used by the chmod(2) syscall) to be set on
136      *    the output file or directory.  group/all rwx modes are stripped even if set
137      *    in this parameter.  If this parameter is negative then neither
138      *    the mode nor the mtime values will be applied to the restored file.
139      * @param mtime A timestamp in the standard Unix epoch that will be imposed as the
140      *    last modification time of the output file.  if the {@code mode} parameter is
141      *    negative then this parameter will be ignored.
142      * @param outFile Location within the filesystem to place the data.  This must point
143      *    to a location that is writeable by the caller, preferably using an absolute path.
144      * @throws IOException
145      */
restoreFile(ParcelFileDescriptor data, long size, int type, long mode, long mtime, File outFile)146     static public void restoreFile(ParcelFileDescriptor data,
147             long size, int type, long mode, long mtime, File outFile) throws IOException {
148         if (type == BackupAgent.TYPE_DIRECTORY) {
149             // Canonically a directory has no associated content, so we don't need to read
150             // anything from the pipe in this case.  Just create the directory here and
151             // drop down to the final metadata adjustment.
152             if (outFile != null) outFile.mkdirs();
153         } else {
154             FileOutputStream out = null;
155 
156             // Pull the data from the pipe, copying it to the output file, until we're done
157             try {
158                 if (outFile != null) {
159                     File parent = outFile.getParentFile();
160                     if (!parent.exists()) {
161                         // in practice this will only be for the default semantic directories,
162                         // and using the default mode for those is appropriate.
163                         // This can also happen for the case where a parent directory has been
164                         // excluded, but a file within that directory has been included.
165                         parent.mkdirs();
166                     }
167                     out = new FileOutputStream(outFile);
168                 }
169             } catch (IOException e) {
170                 Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e);
171             }
172 
173             byte[] buffer = new byte[32 * 1024];
174             final long origSize = size;
175             FileInputStream in = new FileInputStream(data.getFileDescriptor());
176             while (size > 0) {
177                 int toRead = (size > buffer.length) ? buffer.length : (int)size;
178                 int got = in.read(buffer, 0, toRead);
179                 if (got <= 0) {
180                     Log.w(TAG, "Incomplete read: expected " + size + " but got "
181                             + (origSize - size));
182                     break;
183                 }
184                 if (out != null) {
185                     try {
186                         out.write(buffer, 0, got);
187                     } catch (IOException e) {
188                         // Problem writing to the file.  Quit copying data and delete
189                         // the file, but of course keep consuming the input stream.
190                         Log.e(TAG, "Unable to write to file " + outFile.getPath(), e);
191                         out.close();
192                         out = null;
193                         outFile.delete();
194                     }
195                 }
196                 size -= got;
197             }
198             if (out != null) out.close();
199         }
200 
201         // Now twiddle the state to match the backup, assuming all went well
202         if (mode >= 0 && outFile != null) {
203             try {
204                 // explicitly prevent emplacement of files accessible by outside apps
205                 mode &= 0700;
206                 Os.chmod(outFile.getPath(), (int)mode);
207             } catch (ErrnoException e) {
208                 e.rethrowAsIOException();
209             }
210             outFile.setLastModified(mtime);
211         }
212     }
213 
214     @VisibleForTesting
215     public static class BackupScheme {
216         private final File FILES_DIR;
217         private final File DATABASE_DIR;
218         private final File ROOT_DIR;
219         private final File SHAREDPREF_DIR;
220         private final File CACHE_DIR;
221         private final File NOBACKUP_DIR;
222 
223         private final File DEVICE_FILES_DIR;
224         private final File DEVICE_DATABASE_DIR;
225         private final File DEVICE_ROOT_DIR;
226         private final File DEVICE_SHAREDPREF_DIR;
227         private final File DEVICE_CACHE_DIR;
228         private final File DEVICE_NOBACKUP_DIR;
229 
230         private final File EXTERNAL_DIR;
231 
232         private final static String TAG_INCLUDE = "include";
233         private final static String TAG_EXCLUDE = "exclude";
234 
235         final int mFullBackupContent;
236         final PackageManager mPackageManager;
237         final StorageManager mStorageManager;
238         final String mPackageName;
239 
240         // lazy initialized, only when needed
241         private StorageVolume[] mVolumes = null;
242 
243         /**
244          * Parse out the semantic domains into the correct physical location.
245          */
tokenToDirectoryPath(String domainToken)246         String tokenToDirectoryPath(String domainToken) {
247             try {
248                 if (domainToken.equals(FullBackup.FILES_TREE_TOKEN)) {
249                     return FILES_DIR.getCanonicalPath();
250                 } else if (domainToken.equals(FullBackup.DATABASE_TREE_TOKEN)) {
251                     return DATABASE_DIR.getCanonicalPath();
252                 } else if (domainToken.equals(FullBackup.ROOT_TREE_TOKEN)) {
253                     return ROOT_DIR.getCanonicalPath();
254                 } else if (domainToken.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) {
255                     return SHAREDPREF_DIR.getCanonicalPath();
256                 } else if (domainToken.equals(FullBackup.CACHE_TREE_TOKEN)) {
257                     return CACHE_DIR.getCanonicalPath();
258                 } else if (domainToken.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
259                     return NOBACKUP_DIR.getCanonicalPath();
260                 } else if (domainToken.equals(FullBackup.DEVICE_FILES_TREE_TOKEN)) {
261                     return DEVICE_FILES_DIR.getCanonicalPath();
262                 } else if (domainToken.equals(FullBackup.DEVICE_DATABASE_TREE_TOKEN)) {
263                     return DEVICE_DATABASE_DIR.getCanonicalPath();
264                 } else if (domainToken.equals(FullBackup.DEVICE_ROOT_TREE_TOKEN)) {
265                     return DEVICE_ROOT_DIR.getCanonicalPath();
266                 } else if (domainToken.equals(FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN)) {
267                     return DEVICE_SHAREDPREF_DIR.getCanonicalPath();
268                 } else if (domainToken.equals(FullBackup.DEVICE_CACHE_TREE_TOKEN)) {
269                     return DEVICE_CACHE_DIR.getCanonicalPath();
270                 } else if (domainToken.equals(FullBackup.DEVICE_NO_BACKUP_TREE_TOKEN)) {
271                     return DEVICE_NOBACKUP_DIR.getCanonicalPath();
272                 } else if (domainToken.equals(FullBackup.MANAGED_EXTERNAL_TREE_TOKEN)) {
273                     if (EXTERNAL_DIR != null) {
274                         return EXTERNAL_DIR.getCanonicalPath();
275                     } else {
276                         return null;
277                     }
278                 } else if (domainToken.startsWith(FullBackup.SHARED_PREFIX)) {
279                     return sharedDomainToPath(domainToken);
280                 }
281                 // Not a supported location
282                 Log.i(TAG, "Unrecognized domain " + domainToken);
283                 return null;
284             } catch (Exception e) {
285                 Log.i(TAG, "Error reading directory for domain: " + domainToken);
286                 return null;
287             }
288 
289         }
290 
sharedDomainToPath(String domain)291         private String sharedDomainToPath(String domain) throws IOException {
292             // already known to start with SHARED_PREFIX, so we just look after that
293             final String volume = domain.substring(FullBackup.SHARED_PREFIX.length());
294             final StorageVolume[] volumes = getVolumeList();
295             final int volNum = Integer.parseInt(volume);
296             if (volNum < mVolumes.length) {
297                 return volumes[volNum].getPathFile().getCanonicalPath();
298             }
299             return null;
300         }
301 
getVolumeList()302         private StorageVolume[] getVolumeList() {
303             if (mStorageManager != null) {
304                 if (mVolumes == null) {
305                     mVolumes = mStorageManager.getVolumeList();
306                 }
307             } else {
308                 Log.e(TAG, "Unable to access Storage Manager");
309             }
310             return mVolumes;
311         }
312 
313         /**
314          * Represents a path attribute specified in an <include /> rule along with optional
315          * transport flags required from the transport to include file(s) under that path as
316          * specified by requiredFlags attribute. If optional requiredFlags attribute is not
317          * provided, default requiredFlags to 0.
318          * Note: since our parsing codepaths were the same for <include /> and <exclude /> tags,
319          * this structure is also used for <exclude /> tags to preserve that, however you can expect
320          * the getRequiredFlags() to always return 0 for exclude rules.
321          */
322         public static class PathWithRequiredFlags {
323             private final String mPath;
324             private final int mRequiredFlags;
325 
PathWithRequiredFlags(String path, int requiredFlags)326             public PathWithRequiredFlags(String path, int requiredFlags) {
327                 mPath = path;
328                 mRequiredFlags = requiredFlags;
329             }
330 
getPath()331             public String getPath() {
332                 return mPath;
333             }
334 
getRequiredFlags()335             public int getRequiredFlags() {
336                 return mRequiredFlags;
337             }
338         }
339 
340         /**
341          * A map of domain -> set of pairs (canonical file; required transport flags) in that
342          * domain that are to be included if the transport has decared the required flags.
343          * We keep track of the domain so that we can go through the file system in order later on.
344          */
345         Map<String, Set<PathWithRequiredFlags>> mIncludes;
346 
347         /**
348          * Set that will be populated with pairs (canonical file; requiredFlags=0) for each file or
349          * directory that is to be excluded. Note that for excludes, the requiredFlags attribute is
350          * ignored and the value should be always set to 0.
351          */
352         ArraySet<PathWithRequiredFlags> mExcludes;
353 
BackupScheme(Context context)354         BackupScheme(Context context) {
355             mFullBackupContent = context.getApplicationInfo().fullBackupContent;
356             mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
357             mPackageManager = context.getPackageManager();
358             mPackageName = context.getPackageName();
359 
360             // System apps have control over where their default storage context
361             // is pointed, so we're always explicit when building paths.
362             final Context ceContext = context.createCredentialProtectedStorageContext();
363             FILES_DIR = ceContext.getFilesDir();
364             DATABASE_DIR = ceContext.getDatabasePath("foo").getParentFile();
365             ROOT_DIR = ceContext.getDataDir();
366             SHAREDPREF_DIR = ceContext.getSharedPreferencesPath("foo").getParentFile();
367             CACHE_DIR = ceContext.getCacheDir();
368             NOBACKUP_DIR = ceContext.getNoBackupFilesDir();
369 
370             final Context deContext = context.createDeviceProtectedStorageContext();
371             DEVICE_FILES_DIR = deContext.getFilesDir();
372             DEVICE_DATABASE_DIR = deContext.getDatabasePath("foo").getParentFile();
373             DEVICE_ROOT_DIR = deContext.getDataDir();
374             DEVICE_SHAREDPREF_DIR = deContext.getSharedPreferencesPath("foo").getParentFile();
375             DEVICE_CACHE_DIR = deContext.getCacheDir();
376             DEVICE_NOBACKUP_DIR = deContext.getNoBackupFilesDir();
377 
378             if (android.os.Process.myUid() != Process.SYSTEM_UID) {
379                 EXTERNAL_DIR = context.getExternalFilesDir(null);
380             } else {
381                 EXTERNAL_DIR = null;
382             }
383         }
384 
isFullBackupContentEnabled()385         boolean isFullBackupContentEnabled() {
386             if (mFullBackupContent < 0) {
387                 // android:fullBackupContent="false", bail.
388                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
389                     Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"false\"");
390                 }
391                 return false;
392             }
393             return true;
394         }
395 
396         /**
397          * @return A mapping of domain -> set of pairs (canonical file; required transport flags)
398          * in that domain that are to be included if the transport has decared the required flags.
399          * Each of these paths specifies a file that the client has explicitly included in their
400          * backup set. If this map is empty we will back up the entire data directory (including
401          * managed external storage).
402          */
403         public synchronized Map<String, Set<PathWithRequiredFlags>>
maybeParseAndGetCanonicalIncludePaths()404                 maybeParseAndGetCanonicalIncludePaths() throws IOException, XmlPullParserException {
405             if (mIncludes == null) {
406                 maybeParseBackupSchemeLocked();
407             }
408             return mIncludes;
409         }
410 
411         /**
412          * @return A set of (canonical paths; requiredFlags=0) that are to be excluded from the
413          * backup/restore set.
414          */
maybeParseAndGetCanonicalExcludePaths()415         public synchronized ArraySet<PathWithRequiredFlags> maybeParseAndGetCanonicalExcludePaths()
416                 throws IOException, XmlPullParserException {
417             if (mExcludes == null) {
418                 maybeParseBackupSchemeLocked();
419             }
420             return mExcludes;
421         }
422 
maybeParseBackupSchemeLocked()423         private void maybeParseBackupSchemeLocked() throws IOException, XmlPullParserException {
424             // This not being null is how we know that we've tried to parse the xml already.
425             mIncludes = new ArrayMap<String, Set<PathWithRequiredFlags>>();
426             mExcludes = new ArraySet<PathWithRequiredFlags>();
427 
428             if (mFullBackupContent == 0) {
429                 // android:fullBackupContent="true" which means that we'll do everything.
430                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
431                     Log.v(FullBackup.TAG_XML_PARSER, "android:fullBackupContent - \"true\"");
432                 }
433             } else {
434                 // android:fullBackupContent="@xml/some_resource".
435                 if (Log.isLoggable(FullBackup.TAG_XML_PARSER, Log.VERBOSE)) {
436                     Log.v(FullBackup.TAG_XML_PARSER,
437                             "android:fullBackupContent - found xml resource");
438                 }
439                 XmlResourceParser parser = null;
440                 try {
441                     parser = mPackageManager
442                             .getResourcesForApplication(mPackageName)
443                             .getXml(mFullBackupContent);
444                     parseBackupSchemeFromXmlLocked(parser, mExcludes, mIncludes);
445                 } catch (PackageManager.NameNotFoundException e) {
446                     // Throw it as an IOException
447                     throw new IOException(e);
448                 } finally {
449                     if (parser != null) {
450                         parser.close();
451                     }
452                 }
453             }
454         }
455 
456         @VisibleForTesting
parseBackupSchemeFromXmlLocked(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes)457         public void parseBackupSchemeFromXmlLocked(XmlPullParser parser,
458                                                    Set<PathWithRequiredFlags> excludes,
459                                                    Map<String, Set<PathWithRequiredFlags>> includes)
460                 throws IOException, XmlPullParserException {
461             int event = parser.getEventType(); // START_DOCUMENT
462             while (event != XmlPullParser.START_TAG) {
463                 event = parser.next();
464             }
465 
466             if (!"full-backup-content".equals(parser.getName())) {
467                 throw new XmlPullParserException("Xml file didn't start with correct tag" +
468                         " (<full-backup-content>). Found \"" + parser.getName() + "\"");
469             }
470 
471             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
472                 Log.v(TAG_XML_PARSER, "\n");
473                 Log.v(TAG_XML_PARSER, "====================================================");
474                 Log.v(TAG_XML_PARSER, "Found valid fullBackupContent; parsing xml resource.");
475                 Log.v(TAG_XML_PARSER, "====================================================");
476                 Log.v(TAG_XML_PARSER, "");
477             }
478 
479             while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
480                 switch (event) {
481                     case XmlPullParser.START_TAG:
482                         validateInnerTagContents(parser);
483                         final String domainFromXml = parser.getAttributeValue(null, "domain");
484                         final File domainDirectory = getDirectoryForCriteriaDomain(domainFromXml);
485                         if (domainDirectory == null) {
486                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
487                                 Log.v(TAG_XML_PARSER, "...parsing \"" + parser.getName() + "\": "
488                                         + "domain=\"" + domainFromXml + "\" invalid; skipping");
489                             }
490                             break;
491                         }
492                         final File canonicalFile =
493                                 extractCanonicalFile(domainDirectory,
494                                         parser.getAttributeValue(null, "path"));
495                         if (canonicalFile == null) {
496                             break;
497                         }
498 
499                         int requiredFlags = 0; // no transport flags are required by default
500                         if (TAG_INCLUDE.equals(parser.getName())) {
501                             // requiredFlags are only supported for <include /> tag, for <exclude />
502                             // we should always leave them as the default = 0
503                             requiredFlags = getRequiredFlagsFromString(
504                                     parser.getAttributeValue(null, "requireFlags"));
505                         }
506 
507                         // retrieve the include/exclude set we'll be adding this rule to
508                         Set<PathWithRequiredFlags> activeSet = parseCurrentTagForDomain(
509                                 parser, excludes, includes, domainFromXml);
510                         activeSet.add(new PathWithRequiredFlags(canonicalFile.getCanonicalPath(),
511                                 requiredFlags));
512                         if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
513                             Log.v(TAG_XML_PARSER, "...parsed " + canonicalFile.getCanonicalPath()
514                                     + " for domain \"" + domainFromXml + "\", requiredFlags + \""
515                                     + requiredFlags + "\"");
516                         }
517 
518                         // Special case journal files (not dirs) for sqlite database. frowny-face.
519                         // Note that for a restore, the file is never a directory (b/c it doesn't
520                         // exist). We have no way of knowing a priori whether or not to expect a
521                         // dir, so we add the -journal anyway to be safe.
522                         if ("database".equals(domainFromXml) && !canonicalFile.isDirectory()) {
523                             final String canonicalJournalPath =
524                                     canonicalFile.getCanonicalPath() + "-journal";
525                             activeSet.add(new PathWithRequiredFlags(canonicalJournalPath,
526                                     requiredFlags));
527                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
528                                 Log.v(TAG_XML_PARSER, "...automatically generated "
529                                         + canonicalJournalPath + ". Ignore if nonexistent.");
530                             }
531                             final String canonicalWalPath =
532                                     canonicalFile.getCanonicalPath() + "-wal";
533                             activeSet.add(new PathWithRequiredFlags(canonicalWalPath,
534                                     requiredFlags));
535                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
536                                 Log.v(TAG_XML_PARSER, "...automatically generated "
537                                         + canonicalWalPath + ". Ignore if nonexistent.");
538                             }
539                         }
540 
541                         // Special case for sharedpref files (not dirs) also add ".xml" suffix file.
542                         if ("sharedpref".equals(domainFromXml) && !canonicalFile.isDirectory() &&
543                             !canonicalFile.getCanonicalPath().endsWith(".xml")) {
544                             final String canonicalXmlPath =
545                                     canonicalFile.getCanonicalPath() + ".xml";
546                             activeSet.add(new PathWithRequiredFlags(canonicalXmlPath,
547                                     requiredFlags));
548                             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
549                                 Log.v(TAG_XML_PARSER, "...automatically generated "
550                                         + canonicalXmlPath + ". Ignore if nonexistent.");
551                             }
552                         }
553                 }
554             }
555             if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
556                 Log.v(TAG_XML_PARSER, "\n");
557                 Log.v(TAG_XML_PARSER, "Xml resource parsing complete.");
558                 Log.v(TAG_XML_PARSER, "Final tally.");
559                 Log.v(TAG_XML_PARSER, "Includes:");
560                 if (includes.isEmpty()) {
561                     Log.v(TAG_XML_PARSER, "  ...nothing specified (This means the entirety of app"
562                             + " data minus excludes)");
563                 } else {
564                     for (Map.Entry<String, Set<PathWithRequiredFlags>> entry
565                             : includes.entrySet()) {
566                         Log.v(TAG_XML_PARSER, "  domain=" + entry.getKey());
567                         for (PathWithRequiredFlags includeData : entry.getValue()) {
568                             Log.v(TAG_XML_PARSER, " path: " + includeData.getPath()
569                                     + " requiredFlags: " + includeData.getRequiredFlags());
570                         }
571                     }
572                 }
573 
574                 Log.v(TAG_XML_PARSER, "Excludes:");
575                 if (excludes.isEmpty()) {
576                     Log.v(TAG_XML_PARSER, "  ...nothing to exclude.");
577                 } else {
578                     for (PathWithRequiredFlags excludeData : excludes) {
579                         Log.v(TAG_XML_PARSER, " path: " + excludeData.getPath()
580                                 + " requiredFlags: " + excludeData.getRequiredFlags());
581                     }
582                 }
583 
584                 Log.v(TAG_XML_PARSER, "  ");
585                 Log.v(TAG_XML_PARSER, "====================================================");
586                 Log.v(TAG_XML_PARSER, "\n");
587             }
588         }
589 
getRequiredFlagsFromString(String requiredFlags)590         private int getRequiredFlagsFromString(String requiredFlags) {
591             int flags = 0;
592             if (requiredFlags == null || requiredFlags.length() == 0) {
593                 // requiredFlags attribute was missing or empty in <include /> tag
594                 return flags;
595             }
596             String[] flagsStr = requiredFlags.split("\\|");
597             for (String f : flagsStr) {
598                 switch (f) {
599                     case FLAG_REQUIRED_CLIENT_SIDE_ENCRYPTION:
600                         flags |= BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
601                         break;
602                     case FLAG_REQUIRED_DEVICE_TO_DEVICE_TRANSFER:
603                         flags |= BackupAgent.FLAG_DEVICE_TO_DEVICE_TRANSFER;
604                         break;
605                     case FLAG_REQUIRED_FAKE_CLIENT_SIDE_ENCRYPTION:
606                         flags |= BackupAgent.FLAG_FAKE_CLIENT_SIDE_ENCRYPTION_ENABLED;
607                     default:
608                         Log.w(TAG, "Unrecognized requiredFlag provided, value: \"" + f + "\"");
609                 }
610             }
611             return flags;
612         }
613 
parseCurrentTagForDomain(XmlPullParser parser, Set<PathWithRequiredFlags> excludes, Map<String, Set<PathWithRequiredFlags>> includes, String domain)614         private Set<PathWithRequiredFlags> parseCurrentTagForDomain(XmlPullParser parser,
615                 Set<PathWithRequiredFlags> excludes,
616                 Map<String, Set<PathWithRequiredFlags>> includes, String domain)
617                 throws XmlPullParserException {
618             if (TAG_INCLUDE.equals(parser.getName())) {
619                 final String domainToken = getTokenForXmlDomain(domain);
620                 Set<PathWithRequiredFlags> includeSet = includes.get(domainToken);
621                 if (includeSet == null) {
622                     includeSet = new ArraySet<PathWithRequiredFlags>();
623                     includes.put(domainToken, includeSet);
624                 }
625                 return includeSet;
626             } else if (TAG_EXCLUDE.equals(parser.getName())) {
627                 return excludes;
628             } else {
629                 // Unrecognised tag => hard failure.
630                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
631                     Log.v(TAG_XML_PARSER, "Invalid tag found in xml \""
632                             + parser.getName() + "\"; aborting operation.");
633                 }
634                 throw new XmlPullParserException("Unrecognised tag in backup" +
635                         " criteria xml (" + parser.getName() + ")");
636             }
637         }
638 
639         /**
640          * Map xml specified domain (human-readable, what clients put in their manifest's xml) to
641          * BackupAgent internal data token.
642          * @return null if the xml domain was invalid.
643          */
getTokenForXmlDomain(String xmlDomain)644         private String getTokenForXmlDomain(String xmlDomain) {
645             if ("root".equals(xmlDomain)) {
646                 return FullBackup.ROOT_TREE_TOKEN;
647             } else if ("file".equals(xmlDomain)) {
648                 return FullBackup.FILES_TREE_TOKEN;
649             } else if ("database".equals(xmlDomain)) {
650                 return FullBackup.DATABASE_TREE_TOKEN;
651             } else if ("sharedpref".equals(xmlDomain)) {
652                 return FullBackup.SHAREDPREFS_TREE_TOKEN;
653             } else if ("device_root".equals(xmlDomain)) {
654                 return FullBackup.DEVICE_ROOT_TREE_TOKEN;
655             } else if ("device_file".equals(xmlDomain)) {
656                 return FullBackup.DEVICE_FILES_TREE_TOKEN;
657             } else if ("device_database".equals(xmlDomain)) {
658                 return FullBackup.DEVICE_DATABASE_TREE_TOKEN;
659             } else if ("device_sharedpref".equals(xmlDomain)) {
660                 return FullBackup.DEVICE_SHAREDPREFS_TREE_TOKEN;
661             } else if ("external".equals(xmlDomain)) {
662                 return FullBackup.MANAGED_EXTERNAL_TREE_TOKEN;
663             } else {
664                 return null;
665             }
666         }
667 
668         /**
669          *
670          * @param domain Directory where the specified file should exist. Not null.
671          * @param filePathFromXml parsed from xml. Not sanitised before calling this function so may
672          *                        be null.
673          * @return The canonical path of the file specified or null if no such file exists.
674          */
extractCanonicalFile(File domain, String filePathFromXml)675         private File extractCanonicalFile(File domain, String filePathFromXml) {
676             if (filePathFromXml == null) {
677                 // Allow things like <include domain="sharedpref"/>
678                 filePathFromXml = "";
679             }
680             if (filePathFromXml.contains("..")) {
681                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
682                     Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
683                             + "\", but the \"..\" path is not permitted; skipping.");
684                 }
685                 return null;
686             }
687             if (filePathFromXml.contains("//")) {
688                 if (Log.isLoggable(TAG_XML_PARSER, Log.VERBOSE)) {
689                     Log.v(TAG_XML_PARSER, "...resolved \"" + domain.getPath() + " " + filePathFromXml
690                             + "\", which contains the invalid \"//\" sequence; skipping.");
691                 }
692                 return null;
693             }
694             return new File(domain, filePathFromXml);
695         }
696 
697         /**
698          * @param domain parsed from xml. Not sanitised before calling this function so may be null.
699          * @return The directory relevant to the domain specified.
700          */
getDirectoryForCriteriaDomain(String domain)701         private File getDirectoryForCriteriaDomain(String domain) {
702             if (TextUtils.isEmpty(domain)) {
703                 return null;
704             }
705             if ("file".equals(domain)) {
706                 return FILES_DIR;
707             } else if ("database".equals(domain)) {
708                 return DATABASE_DIR;
709             } else if ("root".equals(domain)) {
710                 return ROOT_DIR;
711             } else if ("sharedpref".equals(domain)) {
712                 return SHAREDPREF_DIR;
713             } else if ("device_file".equals(domain)) {
714                 return DEVICE_FILES_DIR;
715             } else if ("device_database".equals(domain)) {
716                 return DEVICE_DATABASE_DIR;
717             } else if ("device_root".equals(domain)) {
718                 return DEVICE_ROOT_DIR;
719             } else if ("device_sharedpref".equals(domain)) {
720                 return DEVICE_SHAREDPREF_DIR;
721             } else if ("external".equals(domain)) {
722                 return EXTERNAL_DIR;
723             } else {
724                 return null;
725             }
726         }
727 
728         /**
729          * Let's be strict about the type of xml the client can write. If we see anything untoward,
730          * throw an XmlPullParserException.
731          */
validateInnerTagContents(XmlPullParser parser)732         private void validateInnerTagContents(XmlPullParser parser) throws XmlPullParserException {
733             if (parser == null) {
734                 return;
735             }
736             switch (parser.getName()) {
737                 case TAG_INCLUDE:
738                     if (parser.getAttributeCount() > 3) {
739                         throw new XmlPullParserException("At most 3 tag attributes allowed for "
740                                 + "\"include\" tag (\"domain\" & \"path\""
741                                 + " & optional \"requiredFlags\").");
742                     }
743                     break;
744                 case TAG_EXCLUDE:
745                     if (parser.getAttributeCount() > 2) {
746                         throw new XmlPullParserException("At most 2 tag attributes allowed for "
747                                 + "\"exclude\" tag (\"domain\" & \"path\".");
748                     }
749                     break;
750                 default:
751                     throw new XmlPullParserException("A valid tag is one of \"<include/>\" or" +
752                             " \"<exclude/>. You provided \"" + parser.getName() + "\"");
753             }
754         }
755     }
756 }
757