// Copyright 2014 The Bazel Authors. All rights reserved. // // 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.google.devtools.build.lib.cmdline; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import java.util.Objects; import javax.annotation.Nullable; /** * The canonical place to parse and validate Blaze labels. */ public final class LabelValidator { /** Matches punctuation in target names which requires quoting in a blaze query. */ private static final CharMatcher PUNCTUATION_REQUIRING_QUOTING = CharMatcher.anyOf("+,=~# ()$"); /** * Matches punctuation in target names which doesn't require quoting in a blaze query. * * Note that . is also allowed in target names, and doesn't require quoting, but has restrictions * on its surrounding characters; see {@link #validateTargetName(String)}. */ private static final CharMatcher PUNCTUATION_NOT_REQUIRING_QUOTING = CharMatcher.anyOf("_@-"); /** * Matches characters allowed in package name. */ private static final CharMatcher ALLOWED_CHARACTERS_IN_PACKAGE_NAME = CharMatcher.inRange('0', '9') .or(CharMatcher.inRange('a', 'z')) .or(CharMatcher.inRange('A', 'Z')) .or(CharMatcher.anyOf("/-._ $()")) .precomputed(); /** * Matches characters allowed in target names regardless of context. * * Note that the only other characters allowed in target names are / and . but they have * restrictions around surrounding characters; see {@link #validateTargetName(String)}. */ private static final CharMatcher ALWAYS_ALLOWED_TARGET_CHARACTERS = CharMatcher.javaLetterOrDigit() .or(PUNCTUATION_REQUIRING_QUOTING) .or(PUNCTUATION_NOT_REQUIRING_QUOTING) .precomputed(); @VisibleForTesting static final String PACKAGE_NAME_ERROR = "package names may contain only A-Z, a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'"; @VisibleForTesting static final String PACKAGE_NAME_DOT_ERROR = "package name component contains only '.' characters"; /** * Performs validity checking of the specified package name. Returns null on success or an error * message otherwise. * * @param packageName the name of the package * @return null if {@code name} is valid or an error string if any part * of the package name is invalid */ @Nullable public static String validatePackageName(String packageName) { int len = packageName.length(); if (len == 0) { // Empty package name (//:foo). return null; } if (packageName.charAt(0) == '/') { return "package names may not start with '/'"; } if (!ALLOWED_CHARACTERS_IN_PACKAGE_NAME.matchesAllOf(packageName)) { return PACKAGE_NAME_ERROR; } if (packageName.charAt(packageName.length() - 1) == '/') { return "package names may not end with '/'"; } // Check for empty or dot-only package segment boolean nonDot = false; boolean lastSlash = true; // Going backward and marking the last character as being a / so we detect // '.' only package segment. for (int i = len - 1; i >= -1; --i) { char c = (i >= 0) ? packageName.charAt(i) : '/'; if (c == '/') { if (lastSlash) { return "package names may not contain '//' path separators"; } if (!nonDot) { return PACKAGE_NAME_DOT_ERROR; } nonDot = false; lastSlash = true; } else { if (c != '.') { nonDot = true; } lastSlash = false; } } return null; // ok } /** * Performs validity checking of the specified target name. Returns null on success or an error * message otherwise. */ @Nullable public static String validateTargetName(String targetName) { // We allow labels equaling '.' or ending in '/.' for now. If we ever // actually configure the target we will report an error, but they are permitted for // data directories. // Code optimized for the common case: success. int len = targetName.length(); if (len == 0) { return "empty target name"; } // Forbidden start chars: char c = targetName.charAt(0); if (c == '/') { return "target names may not start with '/'"; } else if (c == '.') { if (targetName.startsWith("../") || targetName.equals("..")) { return "target names may not contain up-level references '..'"; } else if (targetName.equals(".")) { return null; // See comment above; ideally should be an error. } else if (targetName.startsWith("./")) { return "target names may not contain '.' as a path segment"; } } // Give a friendly error message on CRs in target names if (targetName.endsWith("\r")) { return "target names may not end with carriage returns " + "(perhaps the input source is CRLF-terminated)"; } for (int ii = 0; ii < len; ++ii) { c = targetName.charAt(ii); if (ALWAYS_ALLOWED_TARGET_CHARACTERS.matches(c)) { continue; } if (c == '.') { continue; } if (c == '/') { if (stringRegionMatch(targetName, "/../", ii)) { return "target names may not contain up-level references '..'"; } else if (stringRegionMatch(targetName, "/./", ii)) { return "target names may not contain '.' as a path segment"; } else if (stringRegionMatch(targetName, "//", ii)) { return "target names may not contain '//' path separators"; } continue; } if (CharMatcher.javaIsoControl().matches(c)) { return "target names may not contain non-printable characters: '" + String.format("\\x%02X", (int) c) + "'"; } return "target names may not contain '" + c + "'"; } // Forbidden end chars: if (c == '.') { if (targetName.endsWith("/..")) { return "target names may not contain up-level references '..'"; } else if (targetName.endsWith("/.")) { return null; // See comment above; ideally should be an error. } } if (c == '/') { return "target names may not end with '/'"; } return null; // ok } // Prefer this implementation over calls to String#subString(), as the latter implies copying // the subregion. private static boolean stringRegionMatch(String fullString, String possibleMatch, int offset) { return fullString.regionMatches(offset, possibleMatch, 0, possibleMatch.length()); } /** * Validate the label and parse it into a pair of package name and target name. If the label is * not valid, it throws an {@link BadLabelException}. * * <p>It accepts these forms of labels: * <pre> * //foo/bar * //foo/bar:quux * //foo/bar: (undocumented, but accepted) * </pre> */ public static PackageAndTarget validateAbsoluteLabel(String absName) throws BadLabelException { PackageAndTarget result = parseAbsoluteLabel(absName); String packageName = result.getPackageName(); String targetName = result.getTargetName(); String error = validatePackageName(packageName); if (error != null) { error = "invalid package name '" + packageName + "': " + error; // This check is just for a more helpful error message, // i.e. valid target name, invalid package name, colon-free label form // used => probably they meant "//foo:bar.c" not "//foo/bar.c". if (packageName.endsWith("/" + targetName)) { error += " (perhaps you meant \":" + targetName + "\"?)"; } throw new BadLabelException(error); } error = validateTargetName(targetName); if (error != null) { error = "invalid target name '" + targetName + "': " + error; throw new BadLabelException(error); } return result; } /** * Returns if the label starts with a repository (@whatever) or a package (//whatever). */ public static boolean isAbsolute(String label) { return label.startsWith("//") || label.startsWith("@"); } /** * Parses the given absolute label by verifying that it starts with "//". If it contains a ':', * then the part after that is the target name within the package, and the part before that (but * without the leading "//") is the package name. However, it performs no validation on these two * pieces. * * <p>Use of this method is generally not recommended. * * @throws NullPointerException if {@code absName} is {@code null} * @throws BadLabelException if {@code absName} starts with "//" */ public static PackageAndTarget parseAbsoluteLabel(String absName) throws BadLabelException { if (!isAbsolute(absName)) { throw new BadLabelException("invalid label: " + absName); } if (absName.startsWith("@")) { int endOfRepo = absName.indexOf("//"); if (endOfRepo < 0) { return new PackageAndTarget("", absName.substring(1)); } absName = absName.substring(endOfRepo); } // Find the package/suffix separation: int colonIndex = absName.indexOf(':'); int splitAt = colonIndex >= 0 ? colonIndex : absName.length(); String packageName = absName.substring("//".length(), splitAt); String suffix = absName.substring(splitAt); // ('suffix' is empty, or starts with a colon.) // "If packagename and version are elided, the colon is not necessary." String targetName = suffix.isEmpty() // Target name is last package segment: (works in slash-free case too.) ? packageName.substring(packageName.lastIndexOf('/') + 1) // Target name is what's after colon: : suffix.substring(1); return new PackageAndTarget(packageName, targetName); } /** * A pair of package and target names. Note that having an instance of this does not imply that * the package or target names are actually valid. */ public static class PackageAndTarget { private final String packageName; private final String targetName; public PackageAndTarget(String packageName, String targetName) { this.packageName = packageName; this.targetName = targetName; } public String getPackageName() { return packageName; } public String getTargetName() { return targetName; } @Override public String toString() { return "//" + packageName + ":" + targetName; } @Override public int hashCode() { return Objects.hash(packageName, targetName); } @Override public boolean equals(Object o) { if (o == null || o.getClass() != getClass()) { return false; } PackageAndTarget otherTarget = (PackageAndTarget) o; return Objects.equals(otherTarget.targetName, targetName) && Objects.equals(otherTarget.packageName, packageName); } } /** * An exception to notify the caller that a label could not be parsed. */ public static class BadLabelException extends Exception { public BadLabelException(String msg) { super(msg); } } }