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