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
17package android.app.backup;
18
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.res.XmlResourceParser;
22import android.os.ParcelFileDescriptor;
23import android.os.Process;
24import android.system.ErrnoException;
25import android.system.Os;
26import android.text.TextUtils;
27import android.util.ArrayMap;
28import android.util.ArraySet;
29import android.util.Log;
30
31import com.android.internal.annotations.VisibleForTesting;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35
36import java.io.File;
37import java.io.FileInputStream;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.util.Map;
41import 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 */
50public 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     */
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
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
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     */
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         */
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
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
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         */
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         */
342        public synchronized ArraySet<String> maybeParseAndGetCanonicalExcludePaths()
343                throws IOException, XmlPullParserException {
344            if (mExcludes == null) {
345                maybeParseBackupSchemeLocked();
346            }
347            return mExcludes;
348        }
349
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
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
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         */
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         */
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         */
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         */
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