| // 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.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.devtools.build.lib.analysis.actions.CommandLineItem; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; |
| import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec; |
| import com.google.devtools.build.lib.skyframe.serialization.SerializationException; |
| import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintable; |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; |
| import com.google.devtools.build.lib.util.FileType; |
| import com.google.devtools.build.lib.util.OS; |
| import com.google.devtools.build.lib.util.StringCanonicalizer; |
| import com.google.protobuf.CodedInputStream; |
| import com.google.protobuf.CodedOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| 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 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, as |
| * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like |
| * \\\\network\\paths and \\\\?\\unc\\paths are not supported. |
| */ |
| @Immutable |
| @javax.annotation.concurrent.Immutable |
| @ThreadSafe |
| public abstract class PathFragment |
| implements Comparable<PathFragment>, |
| Serializable, |
| SkylarkPrintable, |
| FileType.HasFileType, |
| CommandLineItem { |
| private static final Helper HELPER = |
| OS.getCurrent() == OS.WINDOWS ? WindowsPathFragment.HELPER : UnixPathFragment.HELPER; |
| |
| public static final char SEPARATOR_CHAR = HELPER.getPrimarySeparatorChar(); |
| |
| public static final int INVALID_SEGMENT = -1; |
| |
| public static final String ROOT_DIR = "/"; |
| |
| /** An empty path fragment. */ |
| public static final PathFragment EMPTY_FRAGMENT = create(""); |
| |
| /** The path fragment representing the root directory. */ |
| public static final PathFragment ROOT_FRAGMENT = create(ROOT_DIR); |
| |
| public static final ObjectCodec<PathFragment> CODEC = new PathFragmentCodec(); |
| |
| /** |
| * A helper object for manipulating the various internal {@link PathFragment} implementations. |
| * |
| * <p>There will be exactly one {@link Helper} instance used to manipulate all the {@link |
| * PathFragment} instances (see {@link PathFragment#HELPER}). All of the various {@link Helper} |
| * and {@link PathFragment} implementations may assume this property. |
| */ |
| protected abstract static class Helper { |
| /** |
| * Returns whether the two given arrays of segments have the same length and should be |
| * considered have logically equal contents. |
| */ |
| protected final boolean segmentsEqual(String[] segments1, String[] segments2) { |
| return segments1.length == segments2.length |
| && segmentsEqual(segments1.length, segments1, 0, segments2); |
| } |
| |
| /** |
| * Returns whether the {@code length} segments in {@code segments1}, starting at {@code offset1} |
| * should be considered to be logically equal to the first {@code length} segments in {@code |
| * segments2}. |
| */ |
| abstract boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2); |
| |
| /** Returns the comparison result of two {@link PathFragment} instances. */ |
| protected abstract int compare(PathFragment pathFragment1, PathFragment pathFragment2); |
| |
| /** Returns a fresh {@link PathFragment} instance from the given path string. */ |
| abstract PathFragment create(String path); |
| /** |
| * Returns a fresh {@link PathFragment} instance from the given information, taking ownership of |
| * {@code segments} and assuming the {@link String}s within have already been interned. |
| */ |
| abstract PathFragment createAlreadyInterned( |
| char driveLetter, boolean isAbsolute, String[] segments); |
| |
| /** Returns whether {@code c} is a path separator. */ |
| abstract boolean isSeparator(char c); |
| /** Returns the primary path separator. */ |
| abstract char getPrimarySeparatorChar(); |
| /** Return whether the given {@code path} contains a path separator. */ |
| abstract boolean containsSeparatorChar(String path); |
| |
| /** |
| * Splits the given {@code toSegment} into path segments, starting at the given {@code offset}. |
| */ |
| protected final String[] segment(String toSegment, int offset) { |
| int length = toSegment.length(); |
| |
| // 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; |
| } |
| } |
| |
| /** 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 createAlreadyInterned(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 createAlreadyInterned(driveLetter, isAbsolute, internedSegments); |
| } |
| |
| /** |
| * Construct a PathFragment from a java.io.File, which is an absolute or |
| * relative UNIX path. Does not support Windows-style Files. |
| */ |
| public static PathFragment create(File path) { |
| return HELPER.create(path.getPath()); |
| } |
| |
| /** |
| * Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path. |
| */ |
| public static PathFragment create(String path) { |
| return HELPER.create(path); |
| } |
| |
| /** |
| * Constructs a PathFragment, taking ownership of {@code segments} and assuming the {@link |
| * String}s within have already been interned. |
| * |
| * <p>Package-private because it does not perform a defensive copy of the segments array. Used |
| * here in PathFragment, and by Path.asFragment() and Path.relativeTo(). |
| */ |
| static PathFragment createAlreadyInterned( |
| char driveLetter, boolean isAbsolute, String[] segments) { |
| return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments); |
| } |
| |
| /** Returns whether the current {@code path} contains a path separator. */ |
| static boolean containsSeparator(String path) { |
| return HELPER.containsSeparatorChar(path); |
| } |
| |
| /** |
| * Construct a PathFragment from a sequence of other PathFragments. The new fragment will be |
| * absolute iff the first fragment was absolute. |
| */ |
| // TODO(bazel-team): Most usages of this method are wasteful from a garbage perspective. Refactor |
| // to something better. |
| public static PathFragment create(PathFragment first, PathFragment second, PathFragment... more) { |
| String[] segments = new String[sumLengths(first, second, more)]; |
| int offset = 0; |
| offset += addSegmentsTo(segments, offset, first); |
| offset += addSegmentsTo(segments, offset, second); |
| for (PathFragment fragment : more) { |
| offset += addSegmentsTo(segments, offset, fragment); |
| } |
| boolean isAbsolute = first.isAbsolute(); |
| char driveLetter = first.getDriveLetter(); |
| return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments); |
| } |
| |
| // Medium sized builds can easily hold millions of live PathFragments, so the per-instance size of |
| // PathFragment is a concern. |
| // |
| // We have two oop-sized fields (segments, path), and one 4-byte-sized one (hashCode). |
| // |
| // If Blaze is run on a jvm with -XX:+UseCompressedOops, each PathFragment instance is 24 bytes |
| // and so adding any additional field will increase the per-instance size to at least 32 bytes. |
| // |
| // If Blaze is run on a jvm with -XX:-UseCompressedOops, each PathFragment instance is 32 bytes |
| // and so adding any additional field will increase the per-instance size to at least 40 bytes. |
| // |
| // Therefore, do not add any additional fields unless you have considered the memory implications. |
| |
| // The individual path components. |
| // Does *not* include the Windows drive letter. |
| protected final String[] segments; |
| |
| // hashCode and path are lazily initialized but semantically immutable. |
| private int hashCode; |
| private String path; |
| |
| protected PathFragment(String[] segments) { |
| this.segments = segments; |
| } |
| |
| private static int addSegmentsTo(String[] segments, int offset, PathFragment fragment) { |
| int count = fragment.segmentCount(); |
| System.arraycopy(fragment.segments, 0, 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; |
| } |
| |
| protected Object writeReplace() { |
| return new PathFragmentSerializationProxy(toString()); |
| } |
| |
| protected 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(HELPER.getPrimarySeparatorChar())); |
| } |
| } |
| } |
| 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 "." + HELPER.getPrimarySeparatorChar() + getPathString(); |
| } else { |
| return getPathString(); |
| } |
| } |
| |
| /** |
| * 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 = |
| ((getDriveLetter() != '\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; |
| } |
| |
| public static boolean isNormalized(String path) { |
| return PathFragment.create(path).isNormalized(); |
| } |
| |
| /** |
| * 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 HELPER.createAlreadyInterned( |
| getDriveLetter(), 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()) { |
| char driveLetter = getDriveLetter(); |
| return driveLetter == '\0' || otherFragment.getDriveLetter() != '\0' |
| ? otherFragment |
| : createAlreadyInterned(driveLetter, true, otherFragment.segments); |
| } else { |
| return create(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(create(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 #checkBaseName} |
| */ |
| public PathFragment getChild(String baseName) { |
| checkBaseName(baseName); |
| baseName = StringCanonicalizer.intern(baseName); |
| String[] newSegments = Arrays.copyOf(segments, segments.length + 1); |
| newSegments[newSegments.length - 1] = baseName; |
| return createAlreadyInterned(getDriveLetter(), 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); |
| } |
| |
| if (!HELPER.segmentsEqual(ancestorLength, segments, 0, ancestorSegments)) { |
| throw new IllegalArgumentException( |
| "PathFragment " + this + " is not beneath " + ancestorDirectory); |
| } |
| |
| int length = segments.length - ancestorLength; |
| String[] resultSegments = subarray(segments, ancestorLength, length); |
| return createAlreadyInterned('\0', false, resultSegments); |
| } |
| |
| /** |
| * Returns a relative path fragment to this path, relative to {@code path}. |
| */ |
| public PathFragment relativeTo(String path) { |
| return relativeTo(create(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 (isAbsolute() != prefix.isAbsolute() |
| || this.segments.length < prefix.segments.length |
| || (isAbsolute() && getDriveLetter() != prefix.getDriveLetter())) { |
| return false; |
| } |
| return HELPER.segmentsEqual(prefix.segments.length, segments, 0, prefix.segments); |
| } |
| |
| /** |
| * 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; |
| return HELPER.segmentsEqual(suffix.segments.length, segments, offset, suffix.segments); |
| } |
| |
| 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) && isAbsolute(); |
| return ((beginIndex == 0) && (endIndex == count)) |
| ? this |
| : createAlreadyInterned( |
| getDriveLetter(), isAbsolute, subarray(segments, beginIndex, endIndex - beginIndex)); |
| } |
| |
| /** |
| * 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 contains the rest of the original path fragment. |
| * |
| * @param beginIndex the beginning index, inclusive. |
| * @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) { |
| return subFragment(beginIndex, segments.length); |
| } |
| |
| /** |
| * Returns true iff the path represented by this object is absolute. |
| * |
| * <p>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. |
| */ |
| public abstract boolean isAbsolute(); |
| |
| public static boolean isAbsolute(String path) { |
| return PathFragment.create(path).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 abstract String windowsVolume(); |
| |
| /** Return the drive letter or '\0' if not applicable. */ |
| // TODO(bazel-team): This doesn't need to pollute the PathFragment interface (ditto for |
| // windowsVolume). |
| public abstract char getDriveLetter(); |
| |
| public boolean isEmpty() { |
| return segments.length == 0; |
| } |
| |
| /** |
| * 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 HELPER.createAlreadyInterned(getDriveLetter(), false, segments); |
| } |
| |
| @Override |
| public final 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. Note that we don't need to worry about multiple |
| // inefficient reads of 'hashCode' on the same thread since it's non-volatile. |
| // |
| // 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. |
| if (hashCode == 0) { |
| hashCode = computeHashCode(); |
| } |
| return hashCode; |
| } |
| |
| protected abstract int computeHashCode(); |
| |
| @Override |
| public abstract boolean equals(Object other); |
| |
| /** |
| * Compares two PathFragments using the lexicographical order. |
| */ |
| @Override |
| public int compareTo(PathFragment p2) { |
| return HELPER.compare(this, p2); |
| } |
| |
| @Override |
| public String toString() { |
| return getPathString(); |
| } |
| |
| @Override |
| public void repr(SkylarkPrinter printer) { |
| printer.append(getPathString()); |
| } |
| |
| @Override |
| public String filePathForFileTypeMatcher() { |
| return getBaseName(); |
| } |
| |
| @Override |
| public String expandToCommandLine() { |
| return getPathString(); |
| } |
| |
| private static class PathFragmentCodec implements ObjectCodec<PathFragment> { |
| private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized(); |
| |
| @Override |
| public Class<PathFragment> getEncodedClass() { |
| return PathFragment.class; |
| } |
| |
| @Override |
| public void serialize(PathFragment pathFragment, CodedOutputStream codedOut) |
| throws IOException, SerializationException { |
| codedOut.writeInt32NoTag(pathFragment.getDriveLetter()); |
| codedOut.writeBoolNoTag(pathFragment.isAbsolute()); |
| codedOut.writeInt32NoTag(pathFragment.segmentCount()); |
| for (int i = 0; i < pathFragment.segmentCount(); i++) { |
| stringCodec.serialize(pathFragment.getSegment(i), codedOut); |
| } |
| } |
| |
| @Override |
| public PathFragment deserialize(CodedInputStream codedIn) |
| throws IOException, SerializationException { |
| char driveLetter = (char) codedIn.readInt32(); |
| boolean isAbsolute = codedIn.readBool(); |
| int segmentCount = codedIn.readInt32(); |
| String[] segments = new String[segmentCount]; |
| for (int i = 0; i < segmentCount; i++) { |
| segments[i] = stringCodec.deserialize(codedIn); |
| } |
| return PathFragment.create(driveLetter, isAbsolute, segments); |
| } |
| } |
| |
| private static void checkBaseName(String baseName) { |
| if (baseName.length() == 0) { |
| throw new IllegalArgumentException("Child must not be empty string ('')"); |
| } |
| if (baseName.equals(".") || baseName.equals("..")) { |
| throw new IllegalArgumentException("baseName must not be '" + baseName + "'"); |
| } |
| if (baseName.indexOf('/') != -1) { |
| throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'"); |
| } |
| } |
| } |