| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint.checks; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.resources.ResourceFolderType; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Detector.JavaScanner; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.ResourceXmlDetector; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.XmlContext; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Multimap; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import java.util.Collection; |
| import java.util.List; |
| |
| /** |
| * Check which makes sure that a full-backup-content descriptor file is valid and logical |
| */ |
| public class FullBackupContentDetector extends ResourceXmlDetector implements JavaScanner { |
| /** |
| * Validation of {@code <full-backup-content>} XML elements |
| */ |
| public static final Issue ISSUE = Issue.create( |
| "FullBackupContent", //$NON-NLS-1$ |
| "Valid Full Backup Content File", |
| |
| "Ensures that a `<full-backup-content>` file, which is pointed to by a " + |
| "`android:fullBackupContent attribute` in the manifest file, is valid", |
| |
| Category.CORRECTNESS, |
| 5, |
| Severity.FATAL, |
| new Implementation( |
| FullBackupContentDetector.class, |
| Scope.RESOURCE_FILE_SCOPE)); |
| |
| @SuppressWarnings("SpellCheckingInspection") |
| private static final String DOMAIN_SHARED_PREF = "sharedpref"; |
| private static final String DOMAIN_ROOT = "root"; |
| private static final String DOMAIN_FILE = "file"; |
| private static final String DOMAIN_DATABASE = "database"; |
| private static final String DOMAIN_EXTERNAL = "external"; |
| private static final String TAG_EXCLUDE = "exclude"; |
| private static final String TAG_INCLUDE = "include"; |
| private static final String TAG_FULL_BACKUP_CONTENT = "full-backup-content"; |
| private static final String ATTR_PATH = "path"; |
| private static final String ATTR_DOMAIN = "domain"; |
| |
| /** |
| * Constructs a new {@link FullBackupContentDetector} |
| */ |
| public FullBackupContentDetector() { |
| } |
| |
| @Override |
| public boolean appliesTo(@NonNull ResourceFolderType folderType) { |
| return folderType == ResourceFolderType.XML; |
| } |
| |
| @Override |
| public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { |
| Element root = document.getDocumentElement(); |
| if (root == null) { |
| return; |
| } |
| if (!TAG_FULL_BACKUP_CONTENT.equals(root.getTagName())) { |
| return; |
| } |
| |
| List<Element> includes = Lists.newArrayList(); |
| List<Element> excludes = Lists.newArrayList(); |
| NodeList children = root.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node child = children.item(i); |
| if (child.getNodeType() == Node.ELEMENT_NODE) { |
| Element element = (Element) child; |
| String tag = element.getTagName(); |
| if (TAG_INCLUDE.equals(tag)) { |
| includes.add(element); |
| } else if (TAG_EXCLUDE.equals(tag)) { |
| excludes.add(element); |
| } else { |
| // See FullBackup#validateInnerTagContents |
| context.report(ISSUE, element, context.getNameLocation(element), |
| String.format("Unexpected element `<%1$s>`", tag)); |
| } |
| } |
| } |
| |
| Multimap<String, String> includePaths = ArrayListMultimap.create(includes.size(), 4); |
| for (Element include : includes) { |
| String domain = validateDomain(context, include); |
| String path = validatePath(context, include); |
| if (domain == null) { |
| continue; |
| } |
| includePaths.put(domain, path); |
| } |
| |
| for (Element exclude : excludes) { |
| String excludePath = validatePath(context, exclude); |
| if (excludePath.isEmpty()) { |
| continue; |
| } |
| String domain = validateDomain(context, exclude); |
| if (domain == null) { |
| continue; |
| } |
| if (includePaths.isEmpty()) { |
| // There is no <include> anywhere: that means that everything |
| // is considered included and there's no potential prefix mismatch |
| continue; |
| } |
| |
| boolean hasPrefix = false; |
| Collection<String> included = includePaths.get(domain); |
| if (included == null) { |
| continue; |
| } |
| for (String includePath : included) { |
| if (excludePath.startsWith(includePath)) { |
| if (excludePath.equals(includePath)) { |
| Attr pathNode = exclude.getAttributeNode(ATTR_PATH); |
| assert pathNode != null; |
| Location location = context.getValueLocation(pathNode); |
| // Find corresponding include path so we can link to it in the |
| // chained location list |
| for (Element include : includes) { |
| Attr includePathNode = include.getAttributeNode(ATTR_PATH); |
| String includeDomain = include.getAttribute(ATTR_DOMAIN); |
| if (includePathNode != null |
| && excludePath.equals(includePathNode.getValue()) |
| && domain.equals(includeDomain)) { |
| Location earlier = context.getLocation(includePathNode); |
| earlier.setMessage("Unnecessary/conflicting <include>"); |
| location.setSecondary(earlier); |
| } |
| } |
| context.report(ISSUE, exclude, location, |
| String.format("Include `%1$s` is also excluded", excludePath)); |
| } |
| hasPrefix = true; |
| break; |
| } |
| } |
| if (!hasPrefix) { |
| Attr pathNode = exclude.getAttributeNode(ATTR_PATH); |
| assert pathNode != null; |
| context.report(ISSUE, exclude, context.getValueLocation(pathNode), |
| String.format("`%1$s` is not in an included path", excludePath)); |
| } |
| } |
| } |
| |
| @NonNull |
| private static String validatePath(@NonNull XmlContext context, @NonNull Element element) { |
| Attr pathNode = element.getAttributeNode(ATTR_PATH); |
| if (pathNode == null) { |
| return ""; |
| } |
| String value = pathNode.getValue(); |
| if (value.contains("//")) { |
| context.report(ISSUE, element, context.getValueLocation(pathNode), |
| "Paths are not allowed to contain `//`"); |
| } else if (value.contains("..")) { |
| context.report(ISSUE, element, context.getValueLocation(pathNode), |
| "Paths are not allowed to contain `..`"); |
| } else if (value.contains("/")) { |
| String domain = element.getAttribute(ATTR_DOMAIN); |
| if (DOMAIN_SHARED_PREF.equals(domain) || DOMAIN_DATABASE.equals(domain)) { |
| context.report(ISSUE, element, context.getValueLocation(pathNode), |
| String.format("Subdirectories are not allowed for domain `%1$s`", |
| domain)); |
| } |
| } |
| return value; |
| } |
| |
| @Nullable |
| private static String validateDomain(@NonNull XmlContext context, @NonNull Element element) { |
| Attr domainNode = element.getAttributeNode(ATTR_DOMAIN); |
| if (domainNode == null) { |
| context.report(ISSUE, element, context.getLocation(element), |
| String.format("Missing domain attribute, expected one of %1$s", |
| Joiner.on(", ").join(VALID_DOMAINS))); |
| return null; |
| } |
| String domain = domainNode.getValue(); |
| for (String availableDomain : VALID_DOMAINS) { |
| if (availableDomain.equals(domain)) { |
| return domain; |
| } |
| } |
| context.report(ISSUE, element, context.getValueLocation(domainNode), |
| String.format("Unexpected domain `%1$s`, expected one of %2$s", domain, |
| Joiner.on(", ").join(VALID_DOMAINS))); |
| |
| return domain; |
| } |
| |
| /** Valid domains; see FullBackup#getTokenForXmlDomain for authoritative list */ |
| private static final String[] VALID_DOMAINS = new String[] { |
| DOMAIN_ROOT, DOMAIN_FILE, DOMAIN_DATABASE, DOMAIN_SHARED_PREF, DOMAIN_EXTERNAL |
| }; |
| } |