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