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