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