/* * Copyright (C) 2014 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 static com.android.SdkConstants.DOT_PROPERTIES; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Implementation; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.TextFormat; import com.google.common.base.Splitter; import java.io.File; import java.util.Iterator; /** * Check for errors in .property files * <p> * TODO: Warn about bad paths like sdk properties with ' in the path, or suffix of " " etc */ public class PropertyFileDetector extends Detector { /** Property file not escaped */ public static final Issue ESCAPE = Issue.create( "PropertyEscape", //$NON-NLS-1$ "Incorrect property escapes", "All backslashes and colons in .property files must be escaped with " + "a backslash (\\). This means that when writing a Windows path, you " + "must escape the file separators, so the path \\My\\Files should be " + "written as `key=\\\\My\\\\Files.`", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( PropertyFileDetector.class, Scope.PROPERTY_SCOPE)); /** Using HTTP instead of HTTPS for the wrapper */ public static final Issue HTTP = Issue.create( "UsingHttp", //$NON-NLS-1$ "Using HTTP instead of HTTPS", "The Gradle Wrapper is available both via HTTP and HTTPS. HTTPS is more " + "secure since it protects against man-in-the-middle attacks etc. Older " + "projects created in Android Studio used HTTP but we now default to HTTPS " + "and recommend upgrading existing projects.", Category.SECURITY, 6, Severity.WARNING, new Implementation( PropertyFileDetector.class, Scope.PROPERTY_SCOPE)); /** Constructs a new {@link PropertyFileDetector} */ public PropertyFileDetector() { } @Override public boolean appliesTo(@NonNull Context context, @NonNull File file) { return file.getPath().endsWith(DOT_PROPERTIES); } @Override public void run(@NonNull Context context) { String contents = context.getContents(); if (contents == null) { return; } int offset = 0; Iterator<String> iterator = Splitter.on('\n').split(contents).iterator(); String line; for (; iterator.hasNext(); offset += line.length() + 1) { line = iterator.next(); if (line.startsWith("#") || line.startsWith(" ")) { continue; } if (line.indexOf('\\') == -1 && line.indexOf(':') == -1) { continue; } int valueStart = line.indexOf('=') + 1; if (valueStart == 0) { continue; } checkLine(context, contents, line, offset, valueStart); } } private static void checkLine(@NonNull Context context, @NonNull String contents, @NonNull String line, int offset, int valueStart) { String prefix = "distributionUrl=http\\"; if (line.startsWith(prefix)) { String https = "https" + line.substring(prefix.length() - 1); String message = String.format("Replace HTTP with HTTPS for better security; use %1$s", https); int startOffset = offset + valueStart; int endOffset = startOffset + 4; // 4: "http".length() Location location = Location.create(context.file, contents, startOffset, endOffset); context.report(HTTP, location, message); } boolean escaped = false; boolean hadNonPathEscape = false; int errorStart = -1; int errorEnd = -1; StringBuilder path = new StringBuilder(); for (int i = valueStart; i < line.length(); i++) { char c = line.charAt(i); if (c == '\\') { escaped = !escaped; if (escaped) { path.append(c); } } else if (c == ':') { if (!escaped) { hadNonPathEscape = true; if (errorStart < 0) { errorStart = i; } errorEnd = i; } else { escaped = false; } path.append(c); } else { if (escaped) { hadNonPathEscape = true; if (errorStart < 0) { errorStart = i; } errorEnd = i; } escaped = false; path.append(c); } } String pathString = path.toString(); String key = line.substring(0, valueStart); if (hadNonPathEscape && key.endsWith(".dir=") || new File(pathString).exists()) { String escapedPath = suggestEscapes(line.substring(valueStart, line.length())); // NOTE: Keep in sync with {@link #getSuggestedEscape} below String message = "Windows file separators (`\\`) and drive letter " + "separators (':') must be escaped (`\\\\`) in property files; use " + escapedPath; int startOffset = offset + errorStart; int endOffset = offset + errorEnd + 1; Location location = Location.create(context.file, contents, startOffset, endOffset); context.report(ESCAPE, location, message); } } @NonNull static String suggestEscapes(@NonNull String value) { value = value.replace("\\:", ":").replace("\\\\", "\\"); return LintUtils.escapePropertyValue(value); } /** * Returns the escaped string value suggested by the error message which should have * been computed by this lint detector. * * @param message the error message created by this lint detector * @param format the format of the error message * @return the suggested escaped value */ @Nullable public static String getSuggestedEscape(@NonNull String message, @NonNull TextFormat format) { return LintUtils.findSubstring(format.toText(message), "; use ", null); } }