blob: 2bed67db7fcfc6dbf01575fb9040cb4efdf86140 [file] [log] [blame]
// 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.vfs;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.StringCanonicalizer;
import java.io.File;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* This class represents an immutable UNIX filesystem path, which may be absolute or relative. The
* path is maintained as a simple ordered list of path segment strings.
*
* <p>This class is independent from other VFS classes, especially anything requiring native code.
* It is safe to use in places that need simple segmented string path functionality.
*
* <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers
* in front of a path (c:/abc) are supported and such paths are correctly recognized as absolute.
* However, Windows-style backslash separators (C:\\foo\\bar) are explicitly not supported, same
* with advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
*/
@Immutable @ThreadSafe
public final class PathFragment implements Comparable<PathFragment>, Serializable {
public static final int INVALID_SEGMENT = -1;
public static final char SEPARATOR_CHAR = '/';
public static final char EXTRA_SEPARATOR_CHAR =
(OS.getCurrent() == OS.WINDOWS) ? '\\' : '/';
public static final String ROOT_DIR = "/";
/** An empty path fragment. */
public static final PathFragment EMPTY_FRAGMENT = new PathFragment("");
public static final Function<String, PathFragment> TO_PATH_FRAGMENT =
new Function<String, PathFragment>() {
@Override
public PathFragment apply(String str) {
return new PathFragment(str);
}
};
public static final Predicate<PathFragment> IS_ABSOLUTE =
new Predicate<PathFragment>() {
@Override
public boolean apply(PathFragment input) {
return input.isAbsolute();
}
};
private static final Function<PathFragment, String> TO_SAFE_PATH_STRING =
new Function<PathFragment, String>() {
@Override
public String apply(PathFragment path) {
return path.getSafePathString();
}
};
/** Lower-level API. Create a PathFragment, interning segments. */
public static PathFragment create(char driveLetter, boolean isAbsolute, String[] segments) {
String[] internedSegments = new String[segments.length];
for (int i = 0; i < segments.length; i++) {
internedSegments[i] = StringCanonicalizer.intern(segments[i]);
}
return new PathFragment(driveLetter, isAbsolute, internedSegments);
}
/** Same as {@link #create(char, boolean, String[])}, except for {@link List}s of segments. */
public static PathFragment create(char driveLetter, boolean isAbsolute, List<String> segments) {
String[] internedSegments = new String[segments.size()];
for (int i = 0; i < segments.size(); i++) {
internedSegments[i] = StringCanonicalizer.intern(segments.get(i));
}
return new PathFragment(driveLetter, isAbsolute, internedSegments);
}
// We have 3 word-sized fields (segments, hashCode and path), and 2
// byte-sized ones, which fits in 16 bytes. Object sizes are rounded
// to 16 bytes. Medium sized builds can easily hold millions of
// live PathFragments, so do not add further fields on a whim.
// The individual path components.
// Does *not* include the Windows drive letter.
private final String[] segments;
// True both for UNIX-style absolute paths ("/foo") and Windows-style ("C:/foo").
// False for a Windows-style volume label ("C:") which is actually a relative path.
private final boolean isAbsolute;
// Upper case Windows drive letter, or '\0' if none or unknown.
private final char driveLetter;
// hashCode and path are lazily initialized but semantically immutable.
private int hashCode;
private String path;
/**
* Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path.
*/
public PathFragment(String path) {
this.driveLetter =
(OS.getCurrent() == OS.WINDOWS
&& path.length() >= 2
&& path.charAt(1) == ':'
&& Character.isLetter(path.charAt(0)))
? Character.toUpperCase(path.charAt(0))
: '\0';
if (driveLetter != '\0') {
path = path.substring(2);
// TODO(bazel-team): Decide what to do about non-absolute paths with a volume name, e.g. C:x.
}
this.isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
this.segments = segment(path, isAbsolute ? 1 : 0);
}
private static boolean isSeparator(char c) {
return c == SEPARATOR_CHAR || c == EXTRA_SEPARATOR_CHAR;
}
/**
* Construct a PathFragment from a java.io.File, which is an absolute or
* relative UNIX path. Does not support Windows-style Files.
*/
public PathFragment(File path) {
this(path.getPath());
}
/**
* Constructs a PathFragment, taking ownership of segments. Package-private,
* because it does not perform a defensive clone of the segments array. Used
* here in PathFragment, and by Path.asFragment() and Path.relativeTo().
*/
PathFragment(char driveLetter, boolean isAbsolute, String[] segments) {
driveLetter = Character.toUpperCase(driveLetter);
if (OS.getCurrent() == OS.WINDOWS
&& segments.length > 0
&& segments[0].length() == 2
&& Character.toUpperCase(segments[0].charAt(0)) == driveLetter
&& segments[0].charAt(1) == ':') {
throw new IllegalStateException(
String.format(
"the drive letter should not be a path segment; drive='%c', segments=[%s]",
driveLetter, Joiner.on(", ").join(segments)));
}
this.driveLetter = driveLetter;
this.isAbsolute = isAbsolute;
this.segments = segments;
}
/**
* Construct a PathFragment from a sequence of other PathFragments. The new
* fragment will be absolute iff the first fragment was absolute.
*/
public PathFragment(PathFragment first, PathFragment second, PathFragment... more) {
// TODO(bazel-team): The handling of absolute path fragments in this constructor is unexpected.
this.segments = new String[sumLengths(first, second, more)];
int offset = 0;
offset += addSegments(offset, first);
offset += addSegments(offset, second);
for (PathFragment fragment : more) {
offset += addSegments(offset, fragment);
}
this.isAbsolute = first.isAbsolute;
this.driveLetter = first.driveLetter;
}
private int addSegments(int offset, PathFragment fragment) {
int count = fragment.segmentCount();
System.arraycopy(fragment.segments, 0, this.segments, offset, count);
return count;
}
private static int sumLengths(PathFragment first, PathFragment second, PathFragment[] more) {
int total = first.segmentCount() + second.segmentCount();
for (PathFragment fragment : more) {
total += fragment.segmentCount();
}
return total;
}
/**
* Segments the string passed in as argument and returns an array of strings.
* The split is performed along occurrences of (sequences of) the slash
* character.
*
* @param toSegment the string to segment
* @param offset how many characters from the start of the string to ignore.
*/
private static String[] segment(String toSegment, int offset) {
int length = toSegment.length();
// Handle "/" and "" quickly.
if (length == offset) {
return new String[0];
}
// We make two passes through the array of characters: count & alloc,
// because simply using ArrayList was a bottleneck showing up during profiling.
int seg = 0;
int start = offset;
for (int i = offset; i < length; i++) {
if (isSeparator(toSegment.charAt(i))) {
if (i > start) { // to skip repeated separators
seg++;
}
start = i + 1;
}
}
if (start < length) {
seg++;
}
String[] result = new String[seg];
seg = 0;
start = offset;
for (int i = offset; i < length; i++) {
if (isSeparator(toSegment.charAt(i))) {
if (i > start) { // to skip repeated separators
result[seg] = StringCanonicalizer.intern(toSegment.substring(start, i));
seg++;
}
start = i + 1;
}
}
if (start < length) {
result[seg] = StringCanonicalizer.intern(toSegment.substring(start, length));
}
return result;
}
private Object writeReplace() {
return new PathFragmentSerializationProxy(toString());
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Serialization is allowed only by proxy");
}
/**
* Returns the path string using '/' as the name-separator character. Returns "" if the path
* is both relative and empty.
*/
public String getPathString() {
// Double-checked locking works, even without volatile, because path is a String, according to:
// http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
if (path == null) {
synchronized (this) {
if (path == null) {
path = StringCanonicalizer.intern(joinSegments(SEPARATOR_CHAR));
}
}
}
return path;
}
/**
* Returns "." if the path fragment is both relative and empty, or {@link
* #getPathString} otherwise.
*/
// TODO(bazel-team): Change getPathString to do this - this behavior makes more sense.
public String getSafePathString() {
return (!isAbsolute && (segmentCount() == 0)) ? "." : getPathString();
}
/**
* Returns the path string using '/' as the name-separator character, but do so in a way
* unambiguously recognizable as path. In other words, return "." for relative and empty paths,
* and prefix relative paths with one segment by "./".
*
* <p>In this way, a shell will always interpret such a string as path (absolute or relative to
* the working directory) and not as command to be searched for in the search path.
*/
public String getCallablePathString() {
if (isAbsolute) {
return getPathString();
} else if (segmentCount() == 0) {
return ".";
} else if (segmentCount() == 1) {
return "." + SEPARATOR_CHAR + getPathString();
} else {
return getPathString();
}
}
/**
* Returns a sequence consisting of the {@link #getSafePathString()} return of each item in
* {@code fragments}.
*/
public static Iterable<String> safePathStrings(Iterable<PathFragment> fragments) {
return Iterables.transform(fragments, TO_SAFE_PATH_STRING);
}
/** Returns the subset of {@code paths} that start with {@code startingWithPath}. */
public static ImmutableSet<PathFragment> filterPathsStartingWith(Set<PathFragment> paths,
PathFragment startingWithPath) {
return ImmutableSet.copyOf(Iterables.filter(paths, startsWithPredicate(startingWithPath)));
}
public static Predicate<PathFragment> startsWithPredicate(final PathFragment prefix) {
return new Predicate<PathFragment>() {
@Override
public boolean apply(PathFragment pathFragment) {
return pathFragment.startsWith(prefix);
}
};
}
/**
* Throws {@link IllegalArgumentException} if {@code paths} contains any paths that
* are equal to {@code startingWithPath} or that are not beneath {@code startingWithPath}.
*/
public static void checkAllPathsAreUnder(Iterable<PathFragment> paths,
PathFragment startingWithPath) {
for (PathFragment path : paths) {
Preconditions.checkArgument(
!path.equals(startingWithPath) && path.startsWith(startingWithPath),
"%s is not beneath %s", path, startingWithPath);
}
}
private String joinSegments(char separatorChar) {
if (segments.length == 0 && isAbsolute) {
return windowsVolume() + ROOT_DIR;
}
// Profile driven optimization:
// Preallocate a size determined by the number of segments, so that
// we do not have to expand the capacity of the StringBuilder.
// Heuristically, this estimate is right for about 99% of the time.
int estimateSize =
((driveLetter != '\0') ? 2 : 0)
+ ((segments.length == 0) ? 0 : (segments.length + 1) * 20);
StringBuilder result = new StringBuilder(estimateSize);
if (isAbsolute) {
// Only print the Windows volume label if the PathFragment is absolute. Do not print relative
// Windows paths like "C:foo/bar", it would break all kinds of things, e.g. glob().
result.append(windowsVolume());
}
boolean initialSegment = true;
for (String segment : segments) {
if (!initialSegment || isAbsolute) {
result.append(separatorChar);
}
initialSegment = false;
result.append(segment);
}
return result.toString();
}
/**
* Return true iff none of the segments are either "." or "..".
*/
public boolean isNormalized() {
for (String segment : segments) {
if (segment.equals(".") || segment.equals("..")) {
return false;
}
}
return true;
}
/**
* Normalizes the path fragment: removes "." and ".." segments if possible
* (if there are too many ".." segments, the resulting PathFragment will still
* start with "..").
*/
public PathFragment normalize() {
String[] scratchSegments = new String[segments.length];
int segmentCount = 0;
for (String segment : segments) {
switch (segment) {
case ".":
// Just discard it
break;
case "..":
if (segmentCount > 0 && !scratchSegments[segmentCount - 1].equals("..")) {
// Remove the last segment, if there is one and it is not "..". This
// means that the resulting PathFragment can still contain ".."
// segments at the beginning.
segmentCount--;
} else {
scratchSegments[segmentCount++] = segment;
}
break;
default:
scratchSegments[segmentCount++] = segment;
}
}
if (segmentCount == segments.length) {
// Optimization, no new PathFragment needs to be created.
return this;
}
return new PathFragment(driveLetter, isAbsolute,
subarray(scratchSegments, 0, segmentCount));
}
/**
* Returns the path formed by appending the relative or absolute path fragment
* {@code otherFragment} to this path.
*
* <p>If {@code otherFragment} is absolute, the current path will be ignored;
* otherwise, they will be concatenated. This is a purely syntactic operation,
* with no path normalization or I/O performed.
*/
public PathFragment getRelative(PathFragment otherFragment) {
if (otherFragment == EMPTY_FRAGMENT) {
return this;
}
if (otherFragment.isAbsolute()) {
return this.driveLetter == '\0' || otherFragment.driveLetter != '\0'
? otherFragment
: new PathFragment(this.driveLetter, true, otherFragment.segments);
} else {
return new PathFragment(this, otherFragment);
}
}
/**
* Returns the path formed by appending the relative or absolute string
* {@code path} to this path.
*
* <p>If the given path string is absolute, the current path will be ignored;
* otherwise, they will be concatenated. This is a purely syntactic operation,
* with no path normalization or I/O performed.
*/
public PathFragment getRelative(String path) {
return getRelative(new PathFragment(path));
}
/**
* Returns the path formed by appending the single non-special segment
* "baseName" to this path.
*
* <p>You should almost always use {@link #getRelative} instead, which has
* the same performance characteristics if the given name is a valid base
* name, and which also works for '.', '..', and strings containing '/'.
*
* @throws IllegalArgumentException if {@code baseName} is not a valid base
* name according to {@link FileSystemUtils#checkBaseName}
*/
public PathFragment getChild(String baseName) {
FileSystemUtils.checkBaseName(baseName);
baseName = StringCanonicalizer.intern(baseName);
String[] newSegments = Arrays.copyOf(segments, segments.length + 1);
newSegments[newSegments.length - 1] = baseName;
return new PathFragment(driveLetter, isAbsolute, newSegments);
}
/**
* Returns the last segment of this path, or "" for the empty fragment.
*/
public String getBaseName() {
return (segments.length == 0) ? "" : segments[segments.length - 1];
}
/**
* Returns the file extension of this path, excluding the period, or "" if there is no extension.
*/
public String getFileExtension() {
String baseName = getBaseName();
int lastIndex = baseName.lastIndexOf('.');
if (lastIndex != -1) {
return baseName.substring(lastIndex + 1);
}
return "";
}
/**
* Returns a relative path fragment to this path, relative to
* {@code ancestorDirectory}.
* <p>
* <code>x.relativeTo(z) == y</code> implies
* <code>z.getRelative(y) == x</code>.
* <p>
* For example, <code>"foo/bar/wiz".relativeTo("foo")</code>
* returns <code>"bar/wiz"</code>.
*/
public PathFragment relativeTo(PathFragment ancestorDirectory) {
String[] ancestorSegments = ancestorDirectory.segments();
int ancestorLength = ancestorSegments.length;
if (isAbsolute != ancestorDirectory.isAbsolute()
|| segments.length < ancestorLength) {
throw new IllegalArgumentException("PathFragment " + this
+ " is not beneath " + ancestorDirectory);
}
for (int index = 0; index < ancestorLength; index++) {
if (!segments[index].equals(ancestorSegments[index])) {
throw new IllegalArgumentException("PathFragment " + this
+ " is not beneath " + ancestorDirectory);
}
}
int length = segments.length - ancestorLength;
String[] resultSegments = subarray(segments, ancestorLength, length);
return new PathFragment('\0', false, resultSegments);
}
/**
* Returns a relative path fragment to this path, relative to {@code path}.
*/
public PathFragment relativeTo(String path) {
return relativeTo(new PathFragment(path));
}
/**
* Returns a new PathFragment formed by appending {@code newName} to the
* parent directory. Null is returned iff this method is called on a
* PathFragment with zero segments. If {@code newName} designates an absolute path,
* the value of {@code this} will be ignored and a PathFragment corresponding to
* {@code newName} will be returned. This behavior is consistent with the behavior of
* {@link #getRelative(String)}.
*/
public PathFragment replaceName(String newName) {
return segments.length == 0 ? null : getParentDirectory().getRelative(newName);
}
/**
* Returns a path representing the parent directory of this path,
* or null iff this Path represents the root of the filesystem.
*
* <p>Note: This method DOES NOT normalize ".." and "." path segments.
*/
public PathFragment getParentDirectory() {
return segments.length == 0 ? null : subFragment(0, segments.length - 1);
}
/**
* Returns true iff {@code prefix}, considered as a list of path segments, is
* a prefix of {@code this}, and that they are both relative or both
* absolute.
*
* <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
* order)
*/
public boolean startsWith(PathFragment prefix) {
if (this.isAbsolute != prefix.isAbsolute
|| this.segments.length < prefix.segments.length
|| (isAbsolute && this.driveLetter != prefix.driveLetter)) {
return false;
}
for (int i = 0, len = prefix.segments.length; i < len; i++) {
if (!this.segments[i].equals(prefix.segments[i])) {
return false;
}
}
return true;
}
/**
* Returns true iff {@code suffix}, considered as a list of path segments, is
* relative and a suffix of {@code this}, or both are absolute and equal.
*
* <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
* order)
*/
public boolean endsWith(PathFragment suffix) {
if ((suffix.isAbsolute && !suffix.equals(this)) ||
this.segments.length < suffix.segments.length) {
return false;
}
int offset = this.segments.length - suffix.segments.length;
for (int i = 0; i < suffix.segments.length; i++) {
if (!this.segments[offset + i].equals(suffix.segments[i])) {
return false;
}
}
return true;
}
private static String[] subarray(String[] array, int start, int length) {
String[] subarray = new String[length];
System.arraycopy(array, start, subarray, 0, length);
return subarray;
}
/**
* Returns a new path fragment that is a sub fragment of this one.
* The sub fragment begins at the specified <code>beginIndex</code> segment
* and ends at the segment at index <code>endIndex - 1</code>. Thus the number
* of segments in the new PathFragment is <code>endIndex - beginIndex</code>.
*
* @param beginIndex the beginning index, inclusive.
* @param endIndex the ending index, exclusive.
* @return the specified sub fragment, never null.
* @exception IndexOutOfBoundsException if the
* <code>beginIndex</code> is negative, or
* <code>endIndex</code> is larger than the length of
* this <code>String</code> object, or
* <code>beginIndex</code> is larger than
* <code>endIndex</code>.
*/
public PathFragment subFragment(int beginIndex, int endIndex) {
int count = segments.length;
if ((beginIndex < 0) || (beginIndex > endIndex) || (endIndex > count)) {
throw new IndexOutOfBoundsException(String.format("path: %s, beginIndex: %d endIndex: %d",
toString(), beginIndex, endIndex));
}
boolean isAbsolute = (beginIndex == 0) && this.isAbsolute;
return ((beginIndex == 0) && (endIndex == count)) ? this :
new PathFragment(driveLetter, isAbsolute,
subarray(segments, beginIndex, endIndex - beginIndex));
}
/**
* Returns true iff the path represented by this object is absolute.
*/
public boolean isAbsolute() {
return isAbsolute;
}
/**
* Returns the segments of this path fragment. This array should not be
* modified.
*/
String[] segments() {
return segments;
}
public ImmutableList<String> getSegments() {
return ImmutableList.copyOf(segments);
}
public String windowsVolume() {
return (driveLetter != '\0') ? driveLetter + ":" : "";
}
/** Return the drive letter or '\0' if not applicable. */
public char getDriveLetter() {
return driveLetter;
}
/**
* Returns the number of segments in this path.
*/
public int segmentCount() {
return segments.length;
}
/**
* Returns the specified segment of this path; index must be positive and
* less than numSegments().
*/
public String getSegment(int index) {
return segments[index];
}
/**
* Returns the index of the first segment which equals one of the input values
* or {@link PathFragment#INVALID_SEGMENT} if none of the segments match.
*/
public int getFirstSegment(Set<String> values) {
for (int i = 0; i < segments.length; i++) {
if (values.contains(segments[i])) {
return i;
}
}
return INVALID_SEGMENT;
}
/**
* Returns true iff this path contains uplevel references "..".
*/
public boolean containsUplevelReferences() {
for (String segment : segments) {
if (segment.equals("..")) {
return true;
}
}
return false;
}
/**
* Returns a relative PathFragment created from this absolute PathFragment using the
* same segments and drive letter.
*/
public PathFragment toRelative() {
Preconditions.checkArgument(isAbsolute);
return new PathFragment(driveLetter, false, segments);
}
private boolean isEmpty() {
return !isAbsolute && segments.length == 0;
}
@Override
public int hashCode() {
// We use the hash code caching strategy employed by java.lang.String. There are three subtle
// things going on here:
//
// (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet.
// Yes, this means that if the hash code is really 0 then we will "recompute" it each time. But
// this isn't a problem in practice since a hash code of 0 is rare.
//
// (2) Since we have no synchronization, multiple threads can race here thinking they are the
// first one to compute and cache the hash code.
//
// (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one
// thread may not be visible by another.
//
// All three of these issues are benign from a correctness perspective; in the end we have no
// overhead from synchronization, at the cost of potentially computing the hash code more than
// once.
int h = hashCode;
if (h == 0) {
h = Boolean.valueOf(isAbsolute).hashCode();
for (String segment : segments) {
h = h * 31 + segment.hashCode();
}
if (!isEmpty()) {
h = h * 31 + Character.valueOf(driveLetter).hashCode();
}
hashCode = h;
}
return h;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof PathFragment)) {
return false;
}
PathFragment otherPath = (PathFragment) other;
if (isEmpty() && otherPath.isEmpty()) {
return true;
} else {
return isAbsolute == otherPath.isAbsolute
&& driveLetter == otherPath.driveLetter
&& Arrays.equals(otherPath.segments, segments);
}
}
/**
* Compares two PathFragments using the lexicographical order.
*/
@Override
public int compareTo(PathFragment p2) {
if (isAbsolute != p2.isAbsolute) {
return isAbsolute ? -1 : 1;
}
PathFragment p1 = this;
String[] segments1 = p1.segments;
String[] segments2 = p2.segments;
int len1 = segments1.length;
int len2 = segments2.length;
int n = Math.min(len1, len2);
for (int i = 0; i < n; i++) {
String segment1 = segments1[i];
String segment2 = segments2[i];
if (!segment1.equals(segment2)) {
return segment1.compareTo(segment2);
}
}
return len1 - len2;
}
@Override
public String toString() {
return getPathString();
}
}