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