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