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