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