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