aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/vfs
diff options
context:
space:
mode:
authorGravatar tomlu <tomlu@google.com>2018-02-08 15:32:00 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2018-02-08 15:34:11 -0800
commita729b9b4c3d7844a7d44934bf3365f92633c0a60 (patch)
tree6329f4baf5b0b83ea6e3bd577b78b8d49afea9f1 /src/main/java/com/google/devtools/build/lib/vfs
parent0ab46f0dd95f735056add4dd8a90a76944b81d00 (diff)
Replace path implementation.
Path and PathFragment have been replaced with String-based implementations. They are pretty similar, but each method is dissimilar enough that I did not feel sharing code was appropriate. A summary of changes: PATH ==== * Subsumes LocalPath (deleted, its tests repurposed) * Use a simple string to back Path * Path instances are no longer interned; Reference equality will no longer work * Always normalized (same as before) * Some operations will now be slower, like instance compares (which were previously just a reference check) * Multiple identical paths will now consume more memory since they are not interned PATH FRAGMENT ============= * Use a simple string to back PathFragment * No more segment arrays with interned strings * Always normalized * Remove isNormalized * Replace some isNormalizied uses with containsUpLevelReferences() to check if path fragments try to escape their scope * To check if user input is normalized, supply static methods on PathFragment to validate the string before constructing a PathFragment * Because PathFragments are always normalized, we have to replace checks for literal "." from PathFragment#getPathString to PathFragment#getSafePathString. The latter returns "." for the empty string. * The previous implementation supported efficient segment semantics (segment count, iterating over segments). This is now expensive since we do longer have a segment array. ARTIFACT ======== * Remove Path instance. It is instead dynamically constructed on request. This is necessary to avoid this CL becoming a memory regression. RELNOTES: None PiperOrigin-RevId: 185062932
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/vfs')
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/BUILD10
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java74
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java39
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java2
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java715
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java144
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/Path.java792
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java12
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java1097
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java3
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java82
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java7
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java64
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java138
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java220
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java240
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java254
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java66
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java4
19 files changed, 1388 insertions, 2575 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/BUILD b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
index f7a57bee03..b829858847 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
@@ -10,8 +10,9 @@ PATH_FRAGMENT_SOURCES = [
"Canonicalizer.java",
"PathFragment.java",
"PathFragmentSerializationProxy.java",
- "UnixPathFragment.java",
- "WindowsPathFragment.java",
+ "OsPathPolicy.java",
+ "UnixOsPathPolicy.java",
+ "WindowsOsPathPolicy.java",
]
java_library(
@@ -25,6 +26,8 @@ java_library(
"//src/main/java/com/google/devtools/build/lib:skylarkinterface",
"//src/main/java/com/google/devtools/build/lib/concurrent",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
+ "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path",
+ "//src/main/java/com/google/devtools/build/lib/windows/jni",
"//third_party:guava",
"//third_party/protobuf:protobuf_java",
],
@@ -47,7 +50,6 @@ java_library(
":pathfragment",
"//src/main/java/com/google/devtools/build/lib:base-util",
"//src/main/java/com/google/devtools/build/lib:filetype",
- "//src/main/java/com/google/devtools/build/lib:os_util",
"//src/main/java/com/google/devtools/build/lib:skylarkinterface",
"//src/main/java/com/google/devtools/build/lib/clock",
"//src/main/java/com/google/devtools/build/lib/concurrent",
@@ -55,8 +57,6 @@ java_library(
"//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
- "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path",
- "//src/main/java/com/google/devtools/build/lib/windows/jni",
"//src/main/java/com/google/devtools/common/options",
"//third_party:guava",
"//third_party:jsr305",
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
index aaaacd93d2..38da81c6d9 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
@@ -23,7 +23,6 @@ import com.google.common.io.ByteSource;
import com.google.common.io.CharStreams;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.vfs.Dirent.Type;
-import com.google.devtools.build.lib.vfs.Path.PathFactory;
import com.google.devtools.common.options.EnumConverter;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -84,25 +83,6 @@ public abstract class FileSystem {
return digestFunction;
}
- private enum UnixPathFactory implements PathFactory {
- INSTANCE {
- @Override
- public Path createRootPath(FileSystem filesystem) {
- return new Path(filesystem, PathFragment.ROOT_DIR, null);
- }
-
- @Override
- public Path createChildPath(Path parent, String childName) {
- return new Path(parent.getFileSystem(), childName, parent);
- }
-
- @Override
- public Path getCachedChildPathInternal(Path path, String childName) {
- return Path.getCachedChildPathInternal(path, childName, /*cacheable=*/ true);
- }
- };
- }
-
/**
* An exception thrown when attempting to resolve an ordinary file as a symlink.
*/
@@ -112,53 +92,23 @@ public abstract class FileSystem {
}
}
- /** Lazy-initialized on first access, always use {@link FileSystem#getRootDirectory} */
- private volatile Path rootPath;
-
private final Root absoluteRoot = new Root.AbsoluteRoot(this);
- /** Returns filesystem-specific path factory. */
- protected PathFactory getPathFactory() {
- return UnixPathFactory.INSTANCE;
- }
-
- /**
- * Returns an absolute path instance, given an absolute path name, without
- * double slashes, .., or . segments. While this method will normalize the
- * path representation by creating a structured/parsed representation, it will
- * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
- * file system.
- */
- public Path getPath(String pathName) {
- return getPath(PathFragment.create(pathName));
- }
-
/**
- * Returns an absolute path instance, given an absolute path name, without
- * double slashes, .., or . segments. While this method will normalize the
- * path representation by creating a structured/parsed representation, it will
- * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
- * file system.
+ * Returns an absolute path instance, given an absolute path name, without double slashes, .., or
+ * . segments. While this method will normalize the path representation by creating a
+ * structured/parsed representation, it will not cause any IO. (e.g., it will not resolve symbolic
+ * links if it's a Unix file system.
*/
- public Path getPath(PathFragment pathName) {
- if (!pathName.isAbsolute()) {
- throw new IllegalArgumentException(pathName.getPathString() + " (not an absolute path)");
- }
- return getRootDirectory().getRelative(pathName);
+ public Path getPath(String path) {
+ return Path.create(path, this);
}
- /**
- * Returns a path representing the root directory of the current file system.
- */
- public final Path getRootDirectory() {
- if (rootPath == null) {
- synchronized (this) {
- if (rootPath == null) {
- rootPath = getPathFactory().createRootPath(this);
- }
- }
- }
- return rootPath;
+ /** Returns an absolute path instance, given an absolute path fragment. */
+ public Path getPath(PathFragment pathFragment) {
+ Preconditions.checkArgument(pathFragment.isAbsolute());
+ return Path.createAlreadyNormalized(
+ pathFragment.getPathString(), pathFragment.getDriveStrLength(), this);
}
final Root getAbsoluteRoot() {
@@ -401,7 +351,7 @@ public abstract class FileSystem {
throw new IOException(naive + " (Too many levels of symbolic links)");
}
if (linkTarget.isAbsolute()) {
- dir = getRootDirectory();
+ dir = getPath(linkTarget.getDriveStr());
}
for (String name : linkTarget.segments()) {
if (name.equals(".") || name.isEmpty()) {
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
index adeb9c847c..b7fd8d2a43 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
@@ -25,7 +25,6 @@ import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
@@ -90,25 +89,13 @@ public class FileSystemUtils {
return a;
}
- /**
- * Returns a path fragment from a given from-dir to a given to-path. May be
- * either a short relative path "foo/bar", an up'n'over relative path
- * "../../foo/bar" or an absolute path.
- */
- public static PathFragment relativePath(Path fromDir, Path to) {
- if (to.getFileSystem() != fromDir.getFileSystem()) {
- throw new IllegalArgumentException("fromDir and to must be on the same FileSystem");
- }
-
- return relativePath(fromDir.asFragment(), to.asFragment());
- }
/**
* Returns a path fragment from a given from-dir to a given to-path.
*/
public static PathFragment relativePath(PathFragment fromDir, PathFragment to) {
if (to.equals(fromDir)) {
- return PathFragment.create("."); // same dir, just return '.'
+ return PathFragment.EMPTY_FRAGMENT;
}
if (to.startsWith(fromDir)) {
return to.relativeTo(fromDir); // easy case--it's a descendant
@@ -883,30 +870,6 @@ public class FileSystemUtils {
}
/**
- * Dumps diagnostic information about the specified filesystem to {@code out}.
- * This is the implementation of the filesystem part of the 'blaze dump'
- * command. It lives here, rather than in DumpCommand, because it requires
- * privileged access to members of this package.
- *
- * <p>Its results are unspecified and MUST NOT be interpreted programmatically.
- */
- public static void dump(FileSystem fs, final PrintStream out) {
- // Unfortunately there's no "letrec" for anonymous functions so we have to
- // (a) name the function, (b) put it in a box and (c) use List not array
- // because of the generic type. *sigh*.
- final List<Predicate<Path>> dumpFunction = new ArrayList<>();
- dumpFunction.add(
- child -> {
- Path path = child;
- out.println(" " + path + " (" + path.toDebugString() + ")");
- path.applyToChildren(dumpFunction.get(0));
- return false;
- });
-
- fs.getRootDirectory().applyToChildren(dumpFunction.get(0));
- }
-
- /**
* Returns the type of the file system path belongs to.
*/
public static String getFileSystem(Path path) {
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
index 5bba53533f..f5b0220e32 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
@@ -281,7 +281,7 @@ public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat {
throws IOException {
java.nio.file.Path nioPath = getNioPath(linkPath);
try {
- Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getPathString()));
+ Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getSafePathString()));
} catch (java.nio.file.FileAlreadyExistsException e) {
throw new IOException(linkPath + ERR_FILE_EXISTS);
} catch (java.nio.file.AccessDeniedException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java b/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
deleted file mode 100644
index a32a4e356c..0000000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
+++ /dev/null
@@ -1,715 +0,0 @@
-// Copyright 2017 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.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-import com.google.devtools.build.lib.util.OS;
-import com.google.devtools.build.lib.windows.WindowsShortPath;
-import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.annotation.Nullable;
-
-/**
- * A local file path representing a file on the host machine. You should use this when you want to
- * access local files via the file system.
- *
- * <p>Paths are either absolute or relative.
- *
- * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
- * slashes ('/') removed, and any trailing slash also removed. The current implementation does not
- * touch the incoming path string unless the string actually needs to be normalized.
- *
- * <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. We are currently using forward
- * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
- * normalization.
- *
- * <p>Mac and Windows file paths are case insensitive. Case is preserved.
- *
- * <p>This class is replaces {@link Path} as the way to access the host machine's file system.
- * Developers should use this class instead of {@link Path}.
- */
-public final class LocalPath implements Comparable<LocalPath> {
- private static final OsPathPolicy DEFAULT_OS = createFilePathOs();
-
- public static final LocalPath EMPTY = create("");
-
- private final String path;
- private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
- private final OsPathPolicy os;
-
- /** Creates a local path that is specific to the host OS. */
- public static LocalPath create(String path) {
- return createWithOs(path, DEFAULT_OS);
- }
-
- @VisibleForTesting
- static LocalPath createWithOs(String path, OsPathPolicy os) {
- Preconditions.checkNotNull(path);
- int normalizationLevel = os.needsToNormalize(path);
- String normalizedPath = os.normalize(path, normalizationLevel);
- int driveStrLength = os.getDriveStrLength(normalizedPath);
- return new LocalPath(normalizedPath, driveStrLength, os);
- }
-
- /** This method expects path to already be normalized. */
- private LocalPath(String path, int driveStrLength, OsPathPolicy os) {
- this.path = Preconditions.checkNotNull(path);
- this.driveStrLength = driveStrLength;
- this.os = Preconditions.checkNotNull(os);
- }
-
- public String getPathString() {
- return path;
- }
-
- /**
- * If called on a {@link LocalPath} instance for a mount name (eg. '/' or 'C:/'), the empty string
- * is returned.
- */
- public String getBaseName() {
- int lastSeparator = path.lastIndexOf(os.getSeparator());
- return lastSeparator < driveStrLength
- ? path.substring(driveStrLength)
- : path.substring(lastSeparator + 1);
- }
-
- /**
- * Returns a {@link LocalPath} instance representing the relative path between this {@link
- * LocalPath} and the given {@link LocalPath}.
- *
- * <pre>
- * Example:
- *
- * LocalPath.create("/foo").getRelative(LocalPath.create("bar/baz"))
- * -> "/foo/bar/baz"
- * </pre>
- *
- * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve
- * symlinks.
- */
- public LocalPath getRelative(LocalPath other) {
- Preconditions.checkNotNull(other);
- Preconditions.checkArgument(os == other.os);
- return getRelative(other.getPathString(), other.driveStrLength);
- }
-
- /**
- * Returns a {@link LocalPath} instance representing the relative path between this {@link
- * LocalPath} and the given path.
- *
- * <p>See {@link #getRelative(LocalPath)} for details.
- */
- public LocalPath getRelative(String other) {
- Preconditions.checkNotNull(other);
- return getRelative(other, os.getDriveStrLength(other));
- }
-
- private LocalPath getRelative(String other, int otherDriveStrLength) {
- if (path.isEmpty()) {
- return create(other);
- }
- if (other.isEmpty()) {
- return this;
- }
- // Note that even if other came from a LocalPath instance we still might
- // need to normalize the result if (for instance) other is a path that
- // starts with '..'
- int normalizationLevel = os.needsToNormalize(other);
- // This is an absolute path, simply return it
- if (otherDriveStrLength > 0) {
- String normalizedPath = os.normalize(other, normalizationLevel);
- return new LocalPath(normalizedPath, otherDriveStrLength, os);
- }
- String newPath;
- if (path.length() == driveStrLength) {
- newPath = path + other;
- } else {
- newPath = path + '/' + other;
- }
- newPath = os.normalize(newPath, normalizationLevel);
- return new LocalPath(newPath, driveStrLength, os);
- }
-
- /**
- * Returns the parent directory of this {@link LocalPath}.
- *
- * <p>If this is called on an single directory for a relative path, this returns an empty relative
- * path. If it's called on a root (like '/') or the empty string, it returns null.
- */
- @Nullable
- public LocalPath getParentDirectory() {
- int lastSeparator = path.lastIndexOf(os.getSeparator());
-
- // For absolute paths we need to specially handle when we hit root
- // Relative paths can't hit this path as driveStrLength == 0
- if (driveStrLength > 0) {
- if (lastSeparator < driveStrLength) {
- if (path.length() > driveStrLength) {
- String newPath = path.substring(0, driveStrLength);
- return new LocalPath(newPath, driveStrLength, os);
- } else {
- return null;
- }
- }
- } else {
- if (lastSeparator == -1) {
- if (!path.isEmpty()) {
- return EMPTY;
- } else {
- return null;
- }
- }
- }
- String newPath = path.substring(0, lastSeparator);
- return new LocalPath(newPath, driveStrLength, os);
- }
-
- /**
- * Returns the {@link LocalPath} relative to the base {@link LocalPath}.
- *
- * <p>For example, <code>LocalPath.create("foo/bar/wiz").relativeTo(LocalPath.create("foo"))
- * </code> returns <code>LocalPath.create("bar/wiz")</code>.
- *
- * <p>If the {@link LocalPath} is not a child of the passed {@link LocalPath} an {@link
- * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
- * LocalPath} instances aren't both absolute or both relative.
- */
- public LocalPath relativeTo(LocalPath base) {
- Preconditions.checkNotNull(base);
- Preconditions.checkArgument(os == base.os);
- if (isAbsolute() != base.isAbsolute()) {
- throw new IllegalArgumentException(
- "Cannot relativize an absolute and a non-absolute path pair");
- }
- String basePath = base.path;
- if (!os.startsWith(path, basePath)) {
- throw new IllegalArgumentException(
- String.format("Path '%s' is not under '%s', cannot relativize", this, base));
- }
- int bn = basePath.length();
- if (bn == 0) {
- return this;
- }
- if (path.length() == bn) {
- return EMPTY;
- }
- final int lastSlashIndex;
- if (basePath.charAt(bn - 1) == '/') {
- lastSlashIndex = bn - 1;
- } else {
- lastSlashIndex = bn;
- }
- if (path.charAt(lastSlashIndex) != '/') {
- throw new IllegalArgumentException(
- String.format("Path '%s' is not under '%s', cannot relativize", this, base));
- }
- String newPath = path.substring(lastSlashIndex + 1);
- return new LocalPath(newPath, 0 /* Always a relative path */, os);
- }
-
- /**
- * Splits a path into its constituent parts. The root is not included. This is an inefficient
- * operation and should be avoided.
- */
- public String[] split() {
- String[] segments = path.split("/");
- if (driveStrLength > 0) {
- // String#split("/") for some reason returns a zero-length array
- // String#split("/hello") returns a 2-length array, so this makes little sense
- if (segments.length == 0) {
- return segments;
- }
- return Arrays.copyOfRange(segments, 1, segments.length);
- }
- return segments;
- }
-
- /**
- * Returns whether this path is an ancestor of another path.
- *
- * <p>A path is considered an ancestor of itself.
- *
- * <p>An absolute path can never be an ancestor of a relative path, and vice versa.
- */
- public boolean startsWith(LocalPath other) {
- Preconditions.checkNotNull(other);
- Preconditions.checkArgument(os == other.os);
- if (other.path.length() > path.length()) {
- return false;
- }
- if (driveStrLength != other.driveStrLength) {
- return false;
- }
- if (!os.startsWith(path, other.path)) {
- return false;
- }
- return path.length() == other.path.length()
- || other.path.length() == driveStrLength
- || path.charAt(other.path.length()) == os.getSeparator();
- }
-
- public boolean isAbsolute() {
- return driveStrLength > 0;
- }
-
- @Override
- public String toString() {
- return path;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- return os.compare(this.path, ((LocalPath) o).path) == 0;
- }
-
- @Override
- public int hashCode() {
- return os.hashPath(this.path);
- }
-
- @Override
- public int compareTo(LocalPath o) {
- return os.compare(this.path, o.path);
- }
-
- /**
- * An interface class representing the differences in path style between different OSs.
- *
- * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc.
- */
- @VisibleForTesting
- interface OsPathPolicy {
- int NORMALIZED = 0; // Path is normalized
- int NEEDS_NORMALIZE = 1; // Path requires normalization
-
- /** Returns required normalization level, passed to {@link #normalize}. */
- int needsToNormalize(String path);
-
- /**
- * Normalizes the passed string according to the passed normalization level.
- *
- * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize}
- */
- String normalize(String path, int normalizationLevel);
-
- /**
- * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'.
- *
- * <p>If the path is relative, 0 is returned
- */
- int getDriveStrLength(String path);
-
- /** Compares two path strings, using the given OS case sensitivity. */
- int compare(String s1, String s2);
-
- /** Computes the hash code for a path string. */
- int hashPath(String s);
-
- /**
- * Returns whether the passed string starts with the given prefix, given the OS case
- * sensitivity.
- *
- * <p>This is a pure string operation and doesn't need to worry about matching path segments.
- */
- boolean startsWith(String path, String prefix);
-
- char getSeparator();
-
- boolean isCaseSensitive();
- }
-
- @VisibleForTesting
- static class UnixOsPathPolicy implements OsPathPolicy {
-
- @Override
- public int needsToNormalize(String path) {
- int n = path.length();
- int dotCount = 0;
- char prevChar = 0;
- for (int i = 0; i < n; i++) {
- char c = path.charAt(i);
- if (c == '/') {
- if (prevChar == '/') {
- return NEEDS_NORMALIZE;
- }
- if (dotCount == 1 || dotCount == 2) {
- return NEEDS_NORMALIZE;
- }
- }
- dotCount = c == '.' ? dotCount + 1 : 0;
- prevChar = c;
- }
- if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
- return NEEDS_NORMALIZE;
- }
- return NORMALIZED;
- }
-
- @Override
- public String normalize(String path, int normalizationLevel) {
- if (normalizationLevel == NORMALIZED) {
- return path;
- }
- if (path.isEmpty()) {
- return path;
- }
- boolean isAbsolute = path.charAt(0) == '/';
- String[] segments = path.split("/+");
- int segmentCount = removeRelativePaths(segments, isAbsolute ? 1 : 0);
- StringBuilder sb = new StringBuilder(path.length());
- if (isAbsolute) {
- sb.append('/');
- }
- for (int i = 0; i < segmentCount; ++i) {
- sb.append(segments[i]);
- sb.append('/');
- }
- if (segmentCount > 0) {
- sb.deleteCharAt(sb.length() - 1);
- }
- return sb.toString();
- }
-
- @Override
- public int getDriveStrLength(String path) {
- if (path.length() == 0) {
- return 0;
- }
- return (path.charAt(0) == '/') ? 1 : 0;
- }
-
- @Override
- public int compare(String s1, String s2) {
- return s1.compareTo(s2);
- }
-
- @Override
- public int hashPath(String s) {
- return s.hashCode();
- }
-
- @Override
- public boolean startsWith(String path, String prefix) {
- return path.startsWith(prefix);
- }
-
- @Override
- public char getSeparator() {
- return '/';
- }
-
- @Override
- public boolean isCaseSensitive() {
- return true;
- }
- }
-
- /** Mac is a unix file system that is case insensitive. */
- @VisibleForTesting
- static class MacOsPathPolicy extends UnixOsPathPolicy {
- @Override
- public int compare(String s1, String s2) {
- return s1.compareToIgnoreCase(s2);
- }
-
- @Override
- public int hashPath(String s) {
- return s.toLowerCase().hashCode();
- }
-
- @Override
- public boolean isCaseSensitive() {
- return false;
- }
- }
-
- @VisibleForTesting
- static class WindowsOsPathPolicy implements OsPathPolicy {
-
- private static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1;
-
- // msys root, used to resolve paths from msys starting with "/"
- private static final AtomicReference<String> UNIX_ROOT = new AtomicReference<>(null);
- private final ShortPathResolver shortPathResolver;
-
- interface ShortPathResolver {
- String resolveShortPath(String path);
- }
-
- static class DefaultShortPathResolver implements ShortPathResolver {
- @Override
- public String resolveShortPath(String path) {
- try {
- return WindowsFileOperations.getLongPath(path);
- } catch (IOException e) {
- return path;
- }
- }
- }
-
- WindowsOsPathPolicy() {
- this(new DefaultShortPathResolver());
- }
-
- WindowsOsPathPolicy(ShortPathResolver shortPathResolver) {
- this.shortPathResolver = shortPathResolver;
- }
-
- @Override
- public int needsToNormalize(String path) {
- int n = path.length();
- int normalizationLevel = 0;
- // Check for unix path
- if (n > 0 && path.charAt(0) == '/') {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
- }
- int dotCount = 0;
- char prevChar = 0;
- int segmentBeginIndex = 0; // The start index of the current path index
- boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test
- for (int i = 0; i < n; i++) {
- char c = path.charAt(i);
- if (c == '/' || c == '\\') {
- if (c == '\\') {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
- }
- // No need to check for '\' here because that already causes normalization
- if (prevChar == '/') {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
- }
- if (dotCount == 1 || dotCount == 2) {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
- }
- if (segmentHasShortPathChar) {
- if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
- }
- }
- segmentBeginIndex = i + 1;
- segmentHasShortPathChar = false;
- } else if (c == '~') {
- // This path segment might be a Windows short path segment
- segmentHasShortPathChar = true;
- }
- dotCount = c == '.' ? dotCount + 1 : 0;
- prevChar = c;
- }
- if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
- normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
- }
- return normalizationLevel;
- }
-
- @Override
- public String normalize(String path, int normalizationLevel) {
- if (normalizationLevel == NORMALIZED) {
- return path;
- }
- if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) {
- String resolvedPath = shortPathResolver.resolveShortPath(path);
- if (resolvedPath != null) {
- path = resolvedPath;
- }
- }
- String[] segments = path.split("[\\\\/]+");
- int driveStrLength = getDriveStrLength(path);
- boolean isAbsolute = driveStrLength > 0;
- int segmentSkipCount = isAbsolute ? 1 : 0;
-
- StringBuilder sb = new StringBuilder(path.length());
- if (isAbsolute) {
- char driveLetter = path.charAt(0);
- sb.append(Character.toUpperCase(driveLetter));
- sb.append(":/");
- }
- // unix path support
- if (!path.isEmpty() && path.charAt(0) == '/') {
- if (path.length() == 2 || (path.length() > 2 && path.charAt(2) == '/')) {
- sb.append(Character.toUpperCase(path.charAt(1)));
- sb.append(":/");
- segmentSkipCount = 2;
- } else {
- String unixRoot = getUnixRoot();
- sb.append(unixRoot);
- }
- }
- int segmentCount = removeRelativePaths(segments, segmentSkipCount);
- for (int i = 0; i < segmentCount; ++i) {
- sb.append(segments[i]);
- sb.append('/');
- }
- if (segmentCount > 0) {
- sb.deleteCharAt(sb.length() - 1);
- }
- return sb.toString();
- }
-
- @Override
- public int getDriveStrLength(String path) {
- int n = path.length();
- if (n < 3) {
- return 0;
- }
- if (isDriveLetter(path.charAt(0))
- && path.charAt(1) == ':'
- && (path.charAt(2) == '/' || path.charAt(2) == '\\')) {
- return 3;
- }
- return 0;
- }
-
- private static boolean isDriveLetter(char c) {
- return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
- }
-
- @Override
- public int compare(String s1, String s2) {
- // Windows is case-insensitive
- return s1.compareToIgnoreCase(s2);
- }
-
- @Override
- public int hashPath(String s) {
- // Windows is case-insensitive
- return s.toLowerCase().hashCode();
- }
-
- @Override
- public boolean startsWith(String path, String prefix) {
- int pathn = path.length();
- int prefixn = prefix.length();
- if (pathn < prefixn) {
- return false;
- }
- for (int i = 0; i < prefixn; ++i) {
- if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public char getSeparator() {
- return '/';
- }
-
- @Override
- public boolean isCaseSensitive() {
- return false;
- }
-
- private String getUnixRoot() {
- String value = UNIX_ROOT.get();
- if (value == null) {
- String jvmFlag = "bazel.windows_unix_root";
- value = determineUnixRoot(jvmFlag);
- if (value == null) {
- throw new IllegalStateException(
- String.format(
- "\"%1$s\" JVM flag is not set. Use the --host_jvm_args flag or export the "
- + "BAZEL_SH environment variable. For example "
- + "\"--host_jvm_args=-D%1$s=c:/tools/msys64\" or "
- + "\"set BAZEL_SH=c:/tools/msys64/usr/bin/bash.exe\".",
- jvmFlag));
- }
- if (getDriveStrLength(value) != 3) {
- throw new IllegalStateException(
- String.format("\"%s\" must be an absolute path, got: \"%s\"", jvmFlag, value));
- }
- value = value.replace('\\', '/');
- if (value.length() > 3 && value.endsWith("/")) {
- value = value.substring(0, value.length() - 1);
- }
- UNIX_ROOT.set(value);
- }
- return value;
- }
-
- private String determineUnixRoot(String jvmArgName) {
- // Get the path from a JVM flag, if specified.
- String path = System.getProperty(jvmArgName);
- if (path == null) {
- return null;
- }
- path = path.trim();
- if (path.isEmpty()) {
- return null;
- }
- return path;
- }
- }
-
- private static OsPathPolicy createFilePathOs() {
- switch (OS.getCurrent()) {
- case LINUX:
- case FREEBSD:
- case UNKNOWN:
- return new UnixOsPathPolicy();
- case DARWIN:
- return new MacOsPathPolicy();
- case WINDOWS:
- return new WindowsOsPathPolicy();
- default:
- throw new AssertionError("Not covering all OSs");
- }
- }
-
- /**
- * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the
- * front. Returns the remaining number of items.
- */
- private static int removeRelativePaths(String[] segments, int starti) {
- int segmentCount = 0;
- int shift = starti;
- for (int i = starti; i < segments.length; ++i) {
- String segment = segments[i];
- switch (segment) {
- case ".":
- // Just discard it
- ++shift;
- break;
- case "..":
- if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) {
- // Remove the last segment, if there is one and it is not "..". This
- // means that the resulting path can still contain ".."
- // segments at the beginning.
- segmentCount--;
- shift += 2;
- break;
- }
- // Fall through
- default:
- ++segmentCount;
- if (shift > 0) {
- segments[i - shift] = segments[i];
- }
- break;
- }
- }
- return segmentCount;
- }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java
new file mode 100644
index 0000000000..6b0380f8c8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java
@@ -0,0 +1,144 @@
+// Copyright 2017 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.devtools.build.lib.util.OS;
+
+/**
+ * An interface class representing the differences in path style between different OSs.
+ *
+ * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc.
+ */
+public interface OsPathPolicy {
+ int NORMALIZED = 0; // Path is normalized
+ int NEEDS_NORMALIZE = 1; // Path requires normalization
+
+ /** Returns required normalization level, passed to {@link #normalize}. */
+ int needsToNormalize(String path);
+
+ /**
+ * Returns the required normalization level if an already normalized string is concatenated with
+ * another normalized path fragment.
+ *
+ * <p>This method may be faster than {@link #needsToNormalize(String)}.
+ */
+ int needsToNormalizeSuffix(String normalizedSuffix);
+
+ /**
+ * Normalizes the passed string according to the passed normalization level.
+ *
+ * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize}
+ */
+ String normalize(String path, int normalizationLevel);
+
+ /**
+ * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'.
+ *
+ * <p>If the path is relative, 0 is returned
+ */
+ int getDriveStrLength(String path);
+
+ /** Compares two path strings, using the given OS case sensitivity. */
+ int compare(String s1, String s2);
+
+ /** Compares two characters, using the given OS case sensitivity. */
+ int compare(char c1, char c2);
+
+ /** Tests two path strings for equality, using the given OS case sensitivity. */
+ boolean equals(String s1, String s2);
+
+ /** Computes the hash code for a path string. */
+ int hash(String s);
+
+ /**
+ * Returns whether the passed string starts with the given prefix, given the OS case sensitivity.
+ *
+ * <p>This is a pure string operation and doesn't need to worry about matching path segments.
+ */
+ boolean startsWith(String path, String prefix);
+
+ /**
+ * Returns whether the passed string ends with the given prefix, given the OS case sensitivity.
+ *
+ * <p>This is a pure string operation and doesn't need to worry about matching path segments.
+ */
+ boolean endsWith(String path, String suffix);
+
+ /** Returns the separator used for normalized paths. */
+ char getSeparator();
+
+ /** Returns whether the unnormalized character c is a separator. */
+ boolean isSeparator(char c);
+
+ boolean isCaseSensitive();
+
+ static OsPathPolicy getFilePathOs() {
+ switch (OS.getCurrent()) {
+ case LINUX:
+ case FREEBSD:
+ case UNKNOWN:
+ return UnixOsPathPolicy.INSTANCE;
+ case DARWIN:
+ // NOTE: We *should* return a path policy that is case insensitive,
+ // but we currently don't handle this
+ return UnixOsPathPolicy.INSTANCE;
+ case WINDOWS:
+ return WindowsOsPathPolicy.INSTANCE;
+ default:
+ throw new AssertionError("Not covering all OSs");
+ }
+ }
+
+ /** Utilities for implementations of {@link OsPathPolicy}. */
+ class Utils {
+ /**
+ * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the
+ * front. Returns the remaining number of items.
+ */
+ static int removeRelativePaths(String[] segments, int starti, boolean isAbsolute) {
+ int segmentCount = 0;
+ int shift = starti;
+ int n = segments.length;
+ for (int i = starti; i < n; ++i) {
+ String segment = segments[i];
+ switch (segment) {
+ case ".":
+ ++shift;
+ break;
+ case "..":
+ if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) {
+ // Remove the last segment, if there is one and it is not "..". This
+ // means that the resulting path can still contain ".."
+ // segments at the beginning.
+ segmentCount--;
+ shift += 2;
+ break;
+ } else if (isAbsolute) {
+ // If this is absolute, then just pop it the ".." off and remain at root
+ ++shift;
+ break;
+ }
+ // Fall through
+ default:
+ ++segmentCount;
+ if (shift > 0) {
+ segments[i - shift] = segments[i];
+ }
+ break;
+ }
+ }
+ return segmentCount;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
index 9549d25bab..075c47bd07 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/Path.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
@@ -14,14 +14,14 @@
package com.google.devtools.build.lib.vfs;
import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodec;
+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.StringCanonicalizer;
import com.google.devtools.build.lib.vfs.FileSystem.HashFunction;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
@@ -33,28 +33,29 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.net.URI;
-import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.IdentityHashMap;
-import java.util.Objects;
+import javax.annotation.Nullable;
/**
- * Instances of this class represent pathnames, forming a tree structure to implement sharing of
- * common prefixes (parent directory names). A node in these trees is something like foo, bar, ..,
- * ., or /. If the instance is not a root path, it will have a parent path. A path can also have
- * children, which are indexed by name in a map.
+ * A local file path representing a file on the host machine. You should use this when you want to
+ * access local files via the file system.
+ *
+ * <p>Paths are always absolute.
+ *
+ * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
+ * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased.
+ * The current implementation does not touch the incoming path string unless the string actually
+ * needs to be normalized.
*
* <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
- * front of a path (c:/abc) are supported. However, Windows-style backslash separators
- * (C:\\foo\\bar) and drive-relative paths ("C:foo") are explicitly not supported, same with
- * advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
+ * 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. We are currently using forward
+ * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
+ * normalization.
*
- * <p>{@link FileSystem} implementations maintain pointers into this graph.
+ * <p>Mac and Windows file paths are case insensitive. Case is preserved.
*/
@ThreadSafe
public class Path
@@ -62,34 +63,6 @@ public class Path
public static final InjectingObjectCodec<Path, FileSystemProvider> CODEC =
new PathCodecWithInjectedFileSystem();
- /** Filesystem-specific factory for {@link Path} objects. */
- public static interface PathFactory {
- /**
- * Creates the root of all paths used by a filesystem.
- *
- * <p>All other paths are instantiated via {@link Path#createChildPath(String)} which calls
- * {@link #createChildPath(Path, String)}.
- *
- * <p>Beware: this is called during the FileSystem constructor which may occur before subclasses
- * are completely initialized.
- */
- Path createRootPath(FileSystem filesystem);
-
- /**
- * Create a child path of the given parent.
- *
- * <p>All {@link Path} objects are instantiated via this method, with the sole exception of the
- * filesystem root, which is created by {@link #createRootPath(FileSystem)}.
- */
- Path createChildPath(Path parent, String childName);
-
- /**
- * Makes the proper invocation of {@link FileSystem#getCachedChildPathInternal}, doing
- * filesystem-specific logic if necessary.
- */
- Path getCachedChildPathInternal(Path path, String childName);
- }
-
private static FileSystem fileSystemForSerialization;
/**
@@ -106,398 +79,279 @@ public class Path
return fileSystemForSerialization;
}
- // These are basically final, but can't be marked as such in order to support serialization.
- private FileSystem fileSystem;
- private String name;
- private Path parent;
- private int depth;
- private int hashCode;
-
- private static final ReferenceQueue<Path> REFERENCE_QUEUE = new ReferenceQueue<>();
+ private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
+ private static final char SEPARATOR = OS.getSeparator();
- private static class PathWeakReferenceForCleanup extends WeakReference<Path> {
- final Path parent;
- final String baseName;
+ private String path;
+ private int driveStrLength; // 1 on Unix, 3 on Windows
+ private FileSystem fileSystem;
- PathWeakReferenceForCleanup(Path referent, ReferenceQueue<Path> referenceQueue) {
- super(referent, referenceQueue);
- parent = referent.getParentDirectory();
- baseName = referent.getBaseName();
- }
+ /** Creates a local path that is specific to the host OS. */
+ static Path create(String path, FileSystem fileSystem) {
+ Preconditions.checkNotNull(path);
+ int normalizationLevel = OS.needsToNormalize(path);
+ String normalizedPath = OS.normalize(path, normalizationLevel);
+ return createAlreadyNormalized(normalizedPath, fileSystem);
}
- private static final Thread PATH_CHILD_CACHE_CLEANUP_THREAD = new Thread("Path cache cleanup") {
- @Override
- public void run() {
- while (true) {
- try {
- PathWeakReferenceForCleanup ref = (PathWeakReferenceForCleanup) REFERENCE_QUEUE.remove();
- Path parent = ref.parent;
- synchronized (parent) {
- // It's possible that since this reference was enqueued for deletion, the Path was
- // recreated with a new entry in the map. We definitely shouldn't delete that entry, so
- // check to make sure they're the same.
- Reference<Path> currentRef = ref.parent.children.get(ref.baseName);
- if (currentRef == ref) {
- ref.parent.children.remove(ref.baseName);
- }
- }
- } catch (InterruptedException e) {
- // Ignored.
- }
- }
- }
- };
-
- static {
- PATH_CHILD_CACHE_CLEANUP_THREAD.setDaemon(true);
- PATH_CHILD_CACHE_CLEANUP_THREAD.start();
+ static Path createAlreadyNormalized(String path, FileSystem fileSystem) {
+ int driveStrLength = OS.getDriveStrLength(path);
+ return createAlreadyNormalized(path, driveStrLength, fileSystem);
}
- /**
- * A mapping from a child file name to the {@link Path} representing it.
- *
- * <p>File names must be a single path segment. The strings must be
- * canonical. We use IdentityHashMap instead of HashMap for reasons of space
- * efficiency: instances are smaller by a single word. Also, since all path
- * segments are interned, the universe of Paths holds a minimal number of
- * references to strings. (It's doubtful that there's any time gain from use
- * of an IdentityHashMap, since the time saved by avoiding string equality
- * tests during hash lookups is probably equal to the time spent eagerly
- * interning strings, unless the collision rate is high.)
- *
- * <p>The Paths are stored as weak references to ensure that a live
- * Path for a directory does not hold a strong reference to all of its
- * descendants, which would prevent collection of paths we never intend to
- * use again. Stale references in the map must be treated as absent.
- *
- * <p>A Path may be recycled once there is no Path that refers to it or
- * to one of its descendants. This means that any data stored in the
- * Path instance by one of its subclasses must be recoverable: it's ok to
- * store data in Paths as an optimization, but there must be another
- * source for that data in case the Path is recycled.
- *
- * <p>We intentionally avoid using the existing library classes for reasons of
- * space efficiency: while ConcurrentHashMap would reduce our locking
- * overhead, and ReferenceMap would simplify the code a little, both of those
- * classes have much higher per-instance overheads than IdentityHashMap.
- *
- * <p>The Path object must be synchronized while children is being
- * accessed.
- */
- private volatile IdentityHashMap<String, Reference<Path>> children;
-
- /**
- * Create a path instance.
- *
- * <p>Should only be called by {@link PathFactory#createChildPath(Path, String)}.
- *
- * @param name the name of this path; it must be canonicalized with {@link
- * StringCanonicalizer#intern}
- * @param parent this path's parent
- */
- protected Path(FileSystem fileSystem, String name, Path parent) {
- this.fileSystem = fileSystem;
- this.name = name;
- this.parent = parent;
- this.depth = parent == null ? 0 : parent.depth + 1;
-
- // No need to include the drive letter in the hash code, because it's derived from the parent
- // and/or the name.
- if (fileSystem == null || fileSystem.isFilePathCaseSensitive()) {
- this.hashCode = Objects.hash(parent, name);
- } else {
- this.hashCode = Objects.hash(parent, name.toLowerCase());
- }
+ static Path createAlreadyNormalized(String path, int driveStrLength, FileSystem fileSystem) {
+ return new Path(path, driveStrLength, fileSystem);
}
- /**
- * Create the root path.
- *
- * <p>Should only be called by {@link PathFactory#createRootPath(FileSystem)}.
- */
- protected Path(FileSystem fileSystem) {
- this(fileSystem, StringCanonicalizer.intern("/"), null);
+ /** This method expects path to already be normalized. */
+ private Path(String path, int driveStrLength, FileSystem fileSystem) {
+ Preconditions.checkArgument(driveStrLength > 0, "Paths must be absolute: '%s'", path);
+ this.path = Preconditions.checkNotNull(path);
+ this.driveStrLength = driveStrLength;
+ this.fileSystem = fileSystem;
}
- private void writeObject(ObjectOutputStream out) throws IOException {
- Preconditions.checkState(
- fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization);
- out.writeUTF(getPathString());
+ public String getPathString() {
+ return path;
}
- private void readObject(ObjectInputStream in) throws IOException {
- fileSystem = fileSystemForSerialization;
- String p = in.readUTF();
- PathFragment pf = PathFragment.create(p);
- PathFragment parentDir = pf.getParentDirectory();
- if (parentDir == null) {
- this.name = "/";
- this.parent = null;
- this.depth = 0;
- } else {
- this.name = pf.getBaseName();
- this.parent = fileSystem.getPath(parentDir);
- this.depth = this.parent.depth + 1;
- }
- this.hashCode = Objects.hash(parent, name);
- reinitializeAfterDeserialization();
+ @Override
+ public String filePathForFileTypeMatcher() {
+ return path;
}
/**
- * Returns the filesystem instance to which this path belongs.
+ * Returns the name of the leaf file or directory.
+ *
+ * <p>If called on a {@link Path} instance for a mount name (eg. '/' or 'C:/'), the empty string
+ * is returned.
*/
- public FileSystem getFileSystem() {
- return fileSystem;
- }
-
- public boolean isRootDirectory() {
- return parent == null;
+ public String getBaseName() {
+ int lastSeparator = path.lastIndexOf(SEPARATOR);
+ return lastSeparator < driveStrLength
+ ? path.substring(driveStrLength)
+ : path.substring(lastSeparator + 1);
}
- protected Path createChildPath(String childName) {
- return fileSystem.getPathFactory().createChildPath(this, childName);
+ /** Synonymous with {@link Path#getRelative(String)}. */
+ public Path getChild(String child) {
+ FileSystemUtils.checkBaseName(child);
+ return getRelative(child);
}
/**
- * Reinitializes this object after deserialization.
- *
- * <p>Derived classes should use this hook to initialize additional state.
+ * Returns a {@link Path} instance representing the relative path between this {@link Path} and
+ * the given path.
*/
- protected void reinitializeAfterDeserialization() {}
-
- /**
- * Returns true if {@code ancestorPath} may be an ancestor of {@code path}.
- *
- * <p>The return value may be a false positive, but it cannot be a false negative. This means that
- * a true return value doesn't mean the ancestor candidate is really an ancestor, however a false
- * return value means it's guaranteed that {@code ancestorCandidate} is not an ancestor of this
- * path.
- *
- * <p>Subclasses may override this method with filesystem-specific logic, e.g. a Windows
- * filesystem may return false if the ancestor path is on a different drive than this one, because
- * it is then guaranteed that the ancestor candidate cannot be an ancestor of this path.
- *
- * @param ancestorCandidate the path that may or may not be an ancestor of this one
- */
- protected boolean isMaybeRelativeTo(Path ancestorCandidate) {
- return true;
+ public Path getRelative(PathFragment other) {
+ Preconditions.checkNotNull(other);
+ String otherStr = other.getPathString();
+ // Fast-path: The path fragment is already normal, use cheaper normalization check
+ return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr));
}
/**
- * Returns true if this directory is top-level, i.e. it is its own parent.
- *
- * <p>When canonicalizing paths the ".." segment of a top-level directory always resolves to the
- * directory itself.
- *
- * <p>On Unix, a top-level directory would be just the filesystem root ("/), on Windows it would
- * be the filesystem root and the volume roots.
+ * Returns a {@link Path} instance representing the relative path between this {@link Path} and
+ * the given path.
*/
- protected boolean isTopLevelDirectory() {
- return isRootDirectory();
+ public Path getRelative(String other) {
+ Preconditions.checkNotNull(other);
+ return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other));
}
- /**
- * Returns the child path named name, or creates such a path (and caches it)
- * if it doesn't already exist.
- */
- private Path getCachedChildPath(String childName) {
- return fileSystem.getPathFactory().getCachedChildPathInternal(this, childName);
+ private Path getRelative(String other, int otherDriveStrLength, int normalizationLevel) {
+ if (other.isEmpty()) {
+ return this;
+ }
+ // This is an absolute path, simply return it
+ if (otherDriveStrLength > 0) {
+ String normalizedPath = OS.normalize(other, normalizationLevel);
+ return new Path(normalizedPath, otherDriveStrLength, fileSystem);
+ }
+ String newPath;
+ if (path.length() == driveStrLength) {
+ newPath = path + other;
+ } else {
+ newPath = path + '/' + other;
+ }
+ // Note that even if other came from a PathFragment instance we still might
+ // need to normalize the result if (for instance) other is a path that
+ // starts with '..'
+ newPath = OS.normalize(newPath, normalizationLevel);
+ return new Path(newPath, driveStrLength, fileSystem);
}
/**
- * Internal method only intended to be called by {@link PathFactory#getCachedChildPathInternal}.
+ * Returns the parent directory of this {@link Path}.
+ *
+ * <p>If called on a root (like '/'), it returns null.
*/
- public static Path getCachedChildPathInternal(Path parent, String childName, boolean cacheable) {
- // We get a canonical instance since 'children' is an IdentityHashMap.
- childName = StringCanonicalizer.intern(childName);
- if (!cacheable) {
- // Non-cacheable children won't show up in `children` so applyToChildren won't run for these.
- return parent.createChildPath(childName);
- }
-
- synchronized (parent) {
- if (parent.children == null) {
- // 66% of Paths have size == 1, 80% <= 2
- parent.children = new IdentityHashMap<>(1);
- }
- Reference<Path> childRef = parent.children.get(childName);
- Path child;
- if (childRef == null || (child = childRef.get()) == null) {
- child = parent.createChildPath(childName);
- parent.children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE));
+ @Nullable
+ public Path getParentDirectory() {
+ int lastSeparator = path.lastIndexOf(SEPARATOR);
+ if (lastSeparator < driveStrLength) {
+ if (path.length() > driveStrLength) {
+ String newPath = path.substring(0, driveStrLength);
+ return new Path(newPath, driveStrLength, fileSystem);
+ } else {
+ return null;
}
- return child;
}
+ String newPath = path.substring(0, lastSeparator);
+ return new Path(newPath, driveStrLength, fileSystem);
}
/**
- * Applies the specified function to each {@link Path} that is an existing direct
- * descendant of this one. The Predicate is evaluated only for its
- * side-effects.
+ * Returns the drive.
*
- * <p>This function exists to hide the "children" field, whose complex
- * synchronization and identity requirements are too unsafe to be exposed to
- * subclasses. For example, the "children" field must be synchronized for
- * the duration of any iteration over it; it may be null; and references
- * within it may be stale, and must be ignored.
+ * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/".
*/
- protected synchronized void applyToChildren(Predicate<Path> function) {
- if (children != null) {
- for (Reference<Path> childRef : children.values()) {
- Path child = childRef.get();
- if (child != null) {
- function.apply(child);
- }
- }
- }
+ public String getDriveStr() {
+ return path.substring(0, driveStrLength);
}
/**
- * Returns whether this path is recursively "under" {@code prefix} - that is,
- * whether {@code path} is a prefix of this path.
+ * Returns the {@link Path} relative to the base {@link Path}.
*
- * <p>This method returns {@code true} when called with this path itself. This
- * method acts independently of the existence of files or folders.
+ * <p>For example, <code>Path.create("foo/bar/wiz").relativeTo(Path.create("foo"))
+ * </code> returns <code>Path.create("bar/wiz")</code>.
*
- * @param prefix a path which may or may not be a prefix of this path
+ * <p>If the {@link Path} is not a child of the passed {@link Path} an {@link
+ * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
+ * Path} instances aren't both absolute or both relative.
*/
- public boolean startsWith(Path prefix) {
- Path n = this;
- for (int i = 0, len = depth - prefix.depth; i < len; i++) {
- n = n.getParentDirectory();
+ public PathFragment relativeTo(Path base) {
+ Preconditions.checkNotNull(base);
+ checkSameFileSystem(base);
+ String basePath = base.path;
+ if (!OS.startsWith(path, basePath)) {
+ throw new IllegalArgumentException(
+ String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+ }
+ int bn = basePath.length();
+ if (bn == 0) {
+ return PathFragment.createAlreadyNormalized(path, driveStrLength);
+ }
+ if (path.length() == bn) {
+ return PathFragment.EMPTY_FRAGMENT;
+ }
+ final int lastSlashIndex;
+ if (basePath.charAt(bn - 1) == '/') {
+ lastSlashIndex = bn - 1;
+ } else {
+ lastSlashIndex = bn;
+ }
+ if (path.charAt(lastSlashIndex) != '/') {
+ throw new IllegalArgumentException(
+ String.format("Path '%s' is not under '%s', cannot relativize", this, base));
}
- return prefix.equals(n);
+ String newPath = path.substring(lastSlashIndex + 1);
+ return PathFragment.createAlreadyNormalized(newPath, 0);
}
/**
- * Computes a string representation of this path, and writes it to the given string builder. Only
- * called locally with a new instance.
+ * Returns whether this path is an ancestor of another path.
+ *
+ * <p>A path is considered an ancestor of itself.
*/
- protected void buildPathString(StringBuilder result) {
- if (isRootDirectory()) {
- result.append(PathFragment.ROOT_DIR);
- } else {
- parent.buildPathString(result);
- if (!parent.isRootDirectory()) {
- result.append(PathFragment.SEPARATOR_CHAR);
- }
- result.append(name);
+ public boolean startsWith(Path other) {
+ if (fileSystem != other.fileSystem) {
+ return false;
}
+ return startsWith(other.path, other.driveStrLength);
}
/**
- * Returns the path encoded as an {@link URI}.
+ * Returns whether this path is an ancestor of another path.
*
- * <p>This concrete implementation returns URIs with "file" as the scheme.
- * For Example:
- * - On Unix the path "/tmp/foo bar.txt" will be encoded as
- * "file:///tmp/foo%20bar.txt".
- * - On Windows the path "C:\Temp\Foo Bar.txt" will be encoded as
- * "file:///C:/Temp/Foo%20Bar.txt"
+ * <p>A path is considered an ancestor of itself.
*
- * <p>Implementors extending this class for special filesystems will likely need to override
- * this method.
- *
- * @throws URISyntaxException if the URI cannot be constructed.
+ * <p>An absolute path can never be an ancestor of a relative path fragment.
*/
- public URI toURI() {
- String ps = getPathString();
- if (!ps.startsWith("/")) {
- // On Windows URI's need to start with a '/'. i.e. C:\Foo\Bar would be file:///C:/Foo/Bar
- ps = "/" + ps;
- }
- try {
- return new URI("file",
- // Needs to be "" instead of null, so that toString() will append "//" after the scheme.
- // We need this for backwards compatibility reasons as some consumers of the BEP are
- // broken.
- "",
- ps, null, null);
- } catch (URISyntaxException e) {
- throw new IllegalStateException(e);
+ public boolean startsWith(PathFragment other) {
+ if (!other.isAbsolute()) {
+ return false;
}
+ String otherPath = other.getPathString();
+ return startsWith(otherPath, OS.getDriveStrLength(otherPath));
}
- /**
- * Returns the path as a string.
- */
- public String getPathString() {
- // Profile driven optimization:
- // Preallocate a size determined by the depth, so that
- // we do not have to expand the capacity of the StringBuilder
- StringBuilder builder = new StringBuilder(depth * 20);
- buildPathString(builder);
- return builder.toString();
+ private boolean startsWith(String otherPath, int otherDriveStrLength) {
+ Preconditions.checkNotNull(otherPath);
+ if (otherPath.length() > path.length()) {
+ return false;
+ }
+ if (driveStrLength != otherDriveStrLength) {
+ return false;
+ }
+ if (!OS.startsWith(path, otherPath)) {
+ return false;
+ }
+ return path.length() == otherPath.length() // Handle equal paths
+ || otherPath.length() == driveStrLength // Handle (eg.) 'C:/foo' starts with 'C:/'
+ // Handle 'true' ancestors, eg. "foo/bar" starts with "foo", but does not start with "fo"
+ || path.charAt(otherPath.length()) == SEPARATOR;
}
- @Override
- public void repr(SkylarkPrinter printer) {
- printer.append(getPathString());
+ public FileSystem getFileSystem() {
+ return fileSystem;
}
- @Override
- public String filePathForFileTypeMatcher() {
- return name;
+ public PathFragment asFragment() {
+ return PathFragment.createAlreadyNormalized(path, driveStrLength);
}
- /**
- * Returns the path as a string.
- */
@Override
public String toString() {
- return getPathString();
+ return path;
}
@Override
- public int hashCode() {
- return hashCode;
- }
-
- @Override
- public boolean equals(Object other) {
- if (this == other) {
+ public boolean equals(Object o) {
+ if (this == o) {
return true;
}
- if (!(other instanceof Path)) {
+ if (o == null || getClass() != o.getClass()) {
return false;
}
-
- Path otherPath = (Path) other;
- if (hashCode != otherPath.hashCode) {
+ Path other = (Path) o;
+ if (fileSystem != other.fileSystem) {
return false;
}
+ return OS.equals(this.path, other.path);
+ }
- if (!fileSystem.equals(otherPath.fileSystem)) {
- return false;
- }
+ @Override
+ public int hashCode() {
+ // Do not include file system for efficiency.
+ // In practice we never construct paths from different file systems.
+ return OS.hash(this.path);
+ }
- if (fileSystem.isFilePathCaseSensitive()) {
- return name.equals(otherPath.name)
- && Objects.equals(parent, otherPath.parent);
- } else {
- return name.toLowerCase().equals(otherPath.name.toLowerCase())
- && Objects.equals(parent, otherPath.parent);
+ @Override
+ public int compareTo(Path o) {
+ // If they are on different file systems, the file system decides the ordering.
+ FileSystem otherFs = o.getFileSystem();
+ if (!fileSystem.equals(otherFs)) {
+ int thisFileSystemHash = System.identityHashCode(fileSystem);
+ int otherFileSystemHash = System.identityHashCode(otherFs);
+ if (thisFileSystemHash < otherFileSystemHash) {
+ return -1;
+ } else if (thisFileSystemHash > otherFileSystemHash) {
+ return 1;
+ }
}
+ return OS.compare(this.path, o.path);
}
- /**
- * Returns a string of debugging information associated with this path.
- * The results are unspecified and MUST NOT be interpreted programmatically.
- */
- protected String toDebugString() {
- return "";
+ @Override
+ public void repr(SkylarkPrinter printer) {
+ printer.append(path);
}
- /**
- * 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 normalises ".." and "." path segments by string
- * processing, not by directory lookups.
- */
- public Path getParentDirectory() {
- return parent;
+ @Override
+ public void str(SkylarkPrinter printer) {
+ repr(printer);
}
/** Returns true iff this path denotes an existing file of any kind. Follows symbolic links. */
@@ -662,157 +516,6 @@ public class Path
}
/**
- * Returns the last segment of this path, or "/" for the root directory.
- */
- public String getBaseName() {
- return name;
- }
-
- /**
- * Interprets the name of a path segment relative to the current path and
- * returns the result.
- *
- * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
- * validate the existence of any path, nor resolve symbolic links. If 'prefix'
- * is not canonical, then a 'name' of '..' will be interpreted incorrectly.
- *
- * @precondition segment contains no slashes.
- */
- private Path getCanonicalPath(String segment) {
- if (segment.equals(".") || segment.isEmpty()) {
- return this; // that's a noop
- } else if (segment.equals("..")) {
- // top-level directory's parent is root, when canonicalising:
- return isTopLevelDirectory() ? this : parent;
- } else {
- return getCachedChildPath(segment);
- }
- }
-
- /**
- * 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 Path getChild(String baseName) {
- FileSystemUtils.checkBaseName(baseName);
- return getCachedChildPath(baseName);
- }
-
- protected Path getRootForRelativePathComputation(PathFragment suffix) {
- return suffix.isAbsolute() ? fileSystem.getRootDirectory() : this;
- }
-
- /**
- * Returns the path formed by appending the relative or absolute path fragment
- * {@code suffix} to this path.
- *
- * <p>If suffix is absolute, the current path will be ignored; otherwise, they
- * will be combined. Up-level references ("..") cause the preceding path
- * segment to be elided; this interpretation is only correct if the base path
- * is canonical.
- */
- public Path getRelative(PathFragment suffix) {
- Path result = getRootForRelativePathComputation(suffix);
- for (String segment : suffix.segments()) {
- result = result.getCanonicalPath(segment);
- }
- return result;
- }
-
- /**
- * 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 combined. Up-level references ("..") cause the
- * preceding path segment to be elided.
- *
- * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
- * validate the existence of any path, nor resolve symbolic links.
- */
- public Path getRelative(String path) {
- // Fast path for valid base names.
- if ((path.length() == 0) || (path.equals("."))) {
- return this;
- } else if (path.equals("..")) {
- return isTopLevelDirectory() ? this : parent;
- } else if (PathFragment.containsSeparator(path)) {
- return getRelative(PathFragment.create(path));
- } else {
- return getCachedChildPath(path);
- }
- }
-
- protected final String[] getSegments() {
- String[] resultSegments = new String[depth];
- Path currentPath = this;
- for (int pos = depth - 1; pos >= 0; pos--) {
- resultSegments[pos] = currentPath.getBaseName();
- currentPath = currentPath.getParentDirectory();
- }
- return resultSegments;
- }
-
- /** Returns an absolute PathFragment representing this path. */
- public PathFragment asFragment() {
- return PathFragment.createAlreadyInterned('\0', true, getSegments());
- }
-
- /**
- * Returns a relative path fragment to this path, relative to {@code
- * ancestorDirectory}. {@code ancestorDirectory} must be on the same
- * filesystem as this path. (Currently, both this path and "ancestorDirectory"
- * must be absolute, though this restriction could be loosened.)
- * <p>
- * <code>x.relativeTo(z) == y</code> implies
- * <code>z.getRelative(y.getPathString()) == x</code>.
- * <p>
- * For example, <code>"/foo/bar/wiz".relativeTo("/foo")</code> returns
- * <code>"bar/wiz"</code>.
- *
- * @throws IllegalArgumentException if this path is not beneath {@code
- * ancestorDirectory} or if they are not part of the same filesystem
- */
- public PathFragment relativeTo(Path ancestorPath) {
- checkSameFilesystem(ancestorPath);
-
- if (isMaybeRelativeTo(ancestorPath)) {
- // Fast path: when otherPath is the ancestor of this path
- int resultSegmentCount = depth - ancestorPath.depth;
- if (resultSegmentCount >= 0) {
- String[] resultSegments = new String[resultSegmentCount];
- Path currentPath = this;
- for (int pos = resultSegmentCount - 1; pos >= 0; pos--) {
- resultSegments[pos] = currentPath.getBaseName();
- currentPath = currentPath.getParentDirectory();
- }
- if (ancestorPath.equals(currentPath)) {
- return PathFragment.createAlreadyInterned('\0', false, resultSegments);
- }
- }
- }
-
- throw new IllegalArgumentException("Path " + this + " is not beneath " + ancestorPath);
- }
-
- /**
- * Checks that "this" and "that" are paths on the same filesystem.
- */
- protected void checkSameFilesystem(Path that) {
- if (this.fileSystem != that.fileSystem) {
- throw new IllegalArgumentException("Files are on different filesystems: "
- + this + ", " + that);
- }
- }
-
- /**
* Returns an output stream to the file denoted by the current path, creating it and truncating it
* if necessary. The stream is opened for writing.
*
@@ -868,7 +571,7 @@ public class Path
* @throws IOException if the creation of the symbolic link was unsuccessful for any reason
*/
public void createSymbolicLink(Path target) throws IOException {
- checkSameFilesystem(target);
+ checkSameFileSystem(target);
fileSystem.createSymbolicLink(this, target.asFragment());
}
@@ -943,7 +646,7 @@ public class Path
* @throws IOException if the rename failed for any reason
*/
public void renameTo(Path target) throws IOException {
- checkSameFilesystem(target);
+ checkSameFileSystem(target);
fileSystem.renameTo(this, target);
}
@@ -1183,69 +886,28 @@ public class Path
fileSystem.prefetchPackageAsync(this, maxDirs);
}
- /**
- * Compare Paths of the same file system using their PathFragments.
- *
- * <p>Paths from different filesystems will be compared using the identity
- * hash code of their respective filesystems.
- */
- @Override
- public int compareTo(Path o) {
- // Fast-path.
- if (equals(o)) {
- return 0;
+ private void checkSameFileSystem(Path that) {
+ if (this.fileSystem != that.fileSystem) {
+ throw new IllegalArgumentException(
+ "Files are on different filesystems: " + this + ", " + that);
}
+ }
- // If they are on different file systems, the file system decides the ordering.
- FileSystem otherFs = o.getFileSystem();
- if (!fileSystem.equals(otherFs)) {
- int thisFileSystemHash = System.identityHashCode(fileSystem);
- int otherFileSystemHash = System.identityHashCode(otherFs);
- if (thisFileSystemHash < otherFileSystemHash) {
- return -1;
- } else if (thisFileSystemHash > otherFileSystemHash) {
- return 1;
- } else {
- // TODO(bazel-team): Add a name to every file system to be used here.
- return 0;
- }
- }
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ Preconditions.checkState(
+ fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization);
+ out.writeUTF(path);
+ }
- // Equal file system, but different paths, because of the canonicalization.
- // We expect to often compare Paths that are very similar, for example for files in the same
- // directory. This can be done efficiently by going up segment by segment until we get the
- // identical path (canonicalization again), and then just compare the immediate child segments.
- // Overall this is much faster than creating PathFragment instances, and comparing those, which
- // requires us to always go up to the top-level directory and copy all segments into a new
- // string array.
- // This was previously showing up as a hotspot in a profile of globbing a large directory.
- Path a = this;
- Path b = o;
- int maxDepth = Math.min(a.depth, b.depth);
- while (a.depth > maxDepth) {
- a = a.getParentDirectory();
- }
- while (b.depth > maxDepth) {
- b = b.getParentDirectory();
- }
- // One is the child of the other.
- if (a.equals(b)) {
- // If a is the same as this, this.depth must be less than o.depth.
- return equals(a) ? -1 : 1;
- }
- Path previousa;
- Path previousb;
- do {
- previousa = a;
- previousb = b;
- a = a.getParentDirectory();
- b = b.getParentDirectory();
- } while (!a.equals(b)); // This has to happen eventually.
- return previousa.name.compareTo(previousb.name);
+ private void readObject(ObjectInputStream in) throws IOException {
+ path = in.readUTF();
+ fileSystem = fileSystemForSerialization;
+ driveStrLength = OS.getDriveStrLength(path);
}
private static class PathCodecWithInjectedFileSystem
implements InjectingObjectCodec<Path, FileSystemProvider> {
+ private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized();
@Override
public Class<Path> getEncodedClass() {
@@ -1256,14 +918,14 @@ public class Path
public void serialize(FileSystemProvider fsProvider, Path path, CodedOutputStream codedOut)
throws IOException, SerializationException {
Preconditions.checkArgument(path.getFileSystem() == fsProvider.getFileSystem());
- PathFragment.CODEC.serialize(path.asFragment(), codedOut);
+ stringCodec.serialize(path.getPathString(), codedOut);
}
@Override
public Path deserialize(FileSystemProvider fsProvider, CodedInputStream codedIn)
throws IOException, SerializationException {
- PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn);
- return fsProvider.getFileSystem().getPath(pathFragment);
+ return Path.createAlreadyNormalized(
+ stringCodec.deserialize(codedIn), fsProvider.getFileSystem());
}
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
index ee0ef364e3..c7644ed960 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
@@ -17,6 +17,7 @@ package com.google.devtools.build.lib.vfs;
import com.google.common.base.Preconditions;
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.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import java.io.IOException;
@@ -24,6 +25,7 @@ import java.io.IOException;
/** Custom serialization for {@link Path}s. */
public class PathCodec implements ObjectCodec<Path> {
+ private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized();
private final FileSystem fileSystem;
/** Create an instance for serializing and deserializing {@link Path}s on {@code fileSystem}. */
@@ -41,15 +43,15 @@ public class PathCodec implements ObjectCodec<Path> {
throws IOException, SerializationException {
Preconditions.checkState(
path.getFileSystem() == fileSystem,
- "Path's FileSystem (%s) did not match the configured FileSystem (%s)",
+ "Path's FileSystem (%s) did not match the configured FileSystem (%s) for path (%s)",
path.getFileSystem(),
- fileSystem);
- PathFragment.CODEC.serialize(path.asFragment(), codedOut);
+ fileSystem,
+ path);
+ stringCodec.serialize(path.getPathString(), codedOut);
}
@Override
public Path deserialize(CodedInputStream codedIn) throws IOException, SerializationException {
- PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn);
- return fileSystem.getPath(pathFragment);
+ return fileSystem.getPath(stringCodec.deserialize(codedIn));
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
index 572042a7d7..88ae732590 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
@@ -1,4 +1,4 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
+// Copyright 2017 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.
@@ -16,630 +16,470 @@ 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;
+import javax.annotation.Nullable;
/**
- * 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.
+ * A path segment representing a path fragment using the host machine's path style. That is; If you
+ * are running on a Unix machine, the path style will be unix, on Windows it is the windows path
+ * style.
*
- * <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>Path fragments are either absolute or relative.
+ *
+ * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
+ * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased.
+ * The current implementation does not touch the incoming path string unless the string actually
+ * needs to be normalized.
*
* <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.
+ * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
+ * slashes ('/') even on Windows.
+ *
+ * <p>Mac and Windows path fragments are case insensitive.
*/
-@Immutable
-@javax.annotation.concurrent.Immutable
-@ThreadSafe
-public abstract class PathFragment
+public final class PathFragment
implements Comparable<PathFragment>,
Serializable,
- SkylarkPrintable,
FileType.HasFileType,
+ SkylarkPrintable,
CommandLineItem {
- private static final Helper HELPER =
- OS.getCurrent() == OS.WINDOWS ? WindowsPathFragment.HELPER : UnixPathFragment.HELPER;
-
- public static final char SEPARATOR_CHAR = HELPER.getPrimarySeparatorChar();
+ private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
- public static final int INVALID_SEGMENT = -1;
-
- public static final String ROOT_DIR = "/";
-
- /** An empty path fragment. */
public static final PathFragment EMPTY_FRAGMENT = create("");
+ public static final char SEPARATOR_CHAR = OS.getSeparator();
+ public static final int INVALID_SEGMENT = -1;
- /** The path fragment representing the root directory. */
- public static final PathFragment ROOT_FRAGMENT = create(ROOT_DIR);
+ private final String path;
+ private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
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.
- */
+ /** Creates a new normalized path fragment. */
public static PathFragment create(String path) {
- return HELPER.create(path);
+ int normalizationLevel = OS.needsToNormalize(path);
+ String normalizedPath =
+ normalizationLevel != OsPathPolicy.NORMALIZED
+ ? OS.normalize(path, normalizationLevel)
+ : path;
+ int driveStrLength = OS.getDriveStrLength(normalizedPath);
+ return new PathFragment(normalizedPath, driveStrLength);
}
/**
- * Constructs a PathFragment, taking ownership of {@code segments} and assuming the {@link
- * String}s within have already been interned.
+ * Creates a new path fragment, where the caller promises that the path is normalized.
*
- * <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().
+ * <p>WARNING! Make sure the path fragment is in fact already normalized. The rest of the code
+ * assumes this is the case.
*/
- 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);
+ public static PathFragment createAlreadyNormalized(String normalizedPath) {
+ int driveStrLength = OS.getDriveStrLength(normalizedPath);
+ return createAlreadyNormalized(normalizedPath, driveStrLength);
}
/**
- * Construct a PathFragment from a sequence of other PathFragments. The new fragment will be
- * absolute iff the first fragment was absolute.
+ * Creates a new path fragment, where the caller promises that the path is normalized.
+ *
+ * <p>Should only be used internally.
*/
- // 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);
+ static PathFragment createAlreadyNormalized(String normalizedPath, int driveStrLength) {
+ return new PathFragment(normalizedPath, driveStrLength);
}
- // 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;
+ /** This method expects path to already be normalized. */
+ private PathFragment(String path, int driveStrLength) {
+ this.path = Preconditions.checkNotNull(path);
+ this.driveStrLength = driveStrLength;
}
- 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;
+ public String getPathString() {
+ return path;
}
- protected Object writeReplace() {
- return new PathFragmentSerializationProxy(toString());
+ public boolean isEmpty() {
+ return path.isEmpty();
}
- protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
- throw new InvalidObjectException("Serialization is allowed only by proxy");
+ int getDriveStrLength() {
+ return driveStrLength;
}
/**
- * Returns the path string using '/' as the name-separator character. Returns "" if the path
- * is both relative and empty.
+ * If called on a {@link PathFragment} instance for a mount name (eg. '/' or 'C:/'), the empty
+ * string is returned.
+ *
+ * <p>This operation allocates a new string.
*/
- 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;
+ public String getBaseName() {
+ int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR);
+ return lastSeparator < driveStrLength
+ ? path.substring(driveStrLength)
+ : path.substring(lastSeparator + 1);
}
/**
- * Returns "." if the path fragment is both relative and empty, or {@link
- * #getPathString} otherwise.
+ * Returns a {@link PathFragment} instance representing the relative path between this {@link
+ * PathFragment} and the given {@link PathFragment}.
+ *
+ * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve
+ * symlinks.
*/
- // TODO(bazel-team): Change getPathString to do this - this behavior makes more sense.
- public String getSafePathString() {
- return (!isAbsolute() && (segmentCount() == 0)) ? "." : getPathString();
+ public PathFragment getRelative(PathFragment other) {
+ Preconditions.checkNotNull(other);
+ // Fast-path: The path fragment is already normal, use cheaper normalization check
+ String otherStr = other.path;
+ return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr));
}
/**
- * 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 "./".
+ * Returns a {@link PathFragment} instance representing the relative path between this {@link
+ * PathFragment} and the given path.
*
- * <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.
+ * <p>See {@link #getRelative(PathFragment)} for details.
*/
- public String getCallablePathString() {
- if (isAbsolute()) {
- return getPathString();
- } else if (segmentCount() == 0) {
- return ".";
- } else if (segmentCount() == 1) {
- return "." + HELPER.getPrimarySeparatorChar() + getPathString();
- } else {
- return getPathString();
- }
+ public PathFragment getRelative(String other) {
+ Preconditions.checkNotNull(other);
+ return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other));
}
- /**
- * 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 PathFragment getRelative(String other, int otherDriveStrLength, int normalizationLevel) {
+ if (path.isEmpty()) {
+ return create(other);
}
- }
-
- private String joinSegments(char separatorChar) {
- if (segments.length == 0 && isAbsolute()) {
- return windowsVolume() + ROOT_DIR;
+ if (other.isEmpty()) {
+ return this;
}
-
- // 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);
+ // This is an absolute path, simply return it
+ if (otherDriveStrLength > 0) {
+ String normalizedPath =
+ normalizationLevel != OsPathPolicy.NORMALIZED
+ ? OS.normalize(other, normalizationLevel)
+ : other;
+ return new PathFragment(normalizedPath, otherDriveStrLength);
+ }
+ String newPath;
+ if (path.length() == driveStrLength) {
+ newPath = path + other;
+ } else {
+ newPath = path + '/' + other;
}
- return result.toString();
+ newPath =
+ normalizationLevel != OsPathPolicy.NORMALIZED
+ ? OS.normalize(newPath, normalizationLevel)
+ : newPath;
+ return new PathFragment(newPath, driveStrLength);
}
- /**
- * Return true iff none of the segments are either "." or "..".
- */
- public boolean isNormalized() {
- for (String segment : segments) {
- if (segment.equals(".") || segment.equals("..")) {
- return false;
- }
+ public PathFragment getChild(String baseName) {
+ checkBaseName(baseName);
+ String newPath;
+ if (path.length() == driveStrLength) {
+ newPath = path + baseName;
+ } else {
+ newPath = path + '/' + baseName;
}
- return true;
- }
-
- public static boolean isNormalized(String path) {
- return PathFragment.create(path).isNormalized();
+ return new PathFragment(newPath, driveStrLength);
}
/**
- * Normalizes the path fragment: removes "." and ".." segments if possible
- * (if there are too many ".." segments, the resulting PathFragment will still
- * start with "..").
+ * Returns the parent directory of this {@link PathFragment}.
+ *
+ * <p>If this is called on an single directory for a relative path, this returns an empty relative
+ * path. If it's called on a root (like '/') or the empty string, it returns null.
*/
- 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;
+ @Nullable
+ public PathFragment getParentDirectory() {
+ int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR);
+
+ // For absolute paths we need to specially handle when we hit root
+ // Relative paths can't hit this path as driveStrLength == 0
+ if (driveStrLength > 0) {
+ if (lastSeparator < driveStrLength) {
+ if (path.length() > driveStrLength) {
+ String newPath = path.substring(0, driveStrLength);
+ return new PathFragment(newPath, driveStrLength);
+ } else {
+ return null;
+ }
+ }
+ } else {
+ if (lastSeparator == -1) {
+ if (!path.isEmpty()) {
+ return EMPTY_FRAGMENT;
+ } else {
+ return null;
+ }
}
}
-
- if (segmentCount == segments.length) {
- // Optimization, no new PathFragment needs to be created.
- return this;
- }
-
- return HELPER.createAlreadyInterned(
- getDriveLetter(), isAbsolute(), subarray(scratchSegments, 0, segmentCount));
+ String newPath = path.substring(0, lastSeparator);
+ return new PathFragment(newPath, driveStrLength);
}
/**
- * Returns the path formed by appending the relative or absolute path fragment
- * {@code otherFragment} to this path.
+ * Returns the {@link PathFragment} relative to the base {@link PathFragment}.
*
- * <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.
+ * <p>For example, <code>FilePath.create("foo/bar/wiz").relativeTo(FilePath.create("foo"))</code>
+ * returns <code>"bar/wiz"</code>.
+ *
+ * <p>If the {@link PathFragment} is not a child of the passed {@link PathFragment} an {@link
+ * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
+ * PathFragment} instances aren't both absolute or both relative.
*/
- public PathFragment getRelative(PathFragment otherFragment) {
- if (otherFragment == EMPTY_FRAGMENT) {
+ public PathFragment relativeTo(PathFragment base) {
+ Preconditions.checkNotNull(base);
+ if (isAbsolute() != base.isAbsolute()) {
+ throw new IllegalArgumentException(
+ "Cannot relativize an absolute and a non-absolute path pair");
+ }
+ String basePath = base.path;
+ if (!OS.startsWith(path, basePath)) {
+ throw new IllegalArgumentException(
+ String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+ }
+ int bn = basePath.length();
+ if (bn == 0) {
return this;
}
-
- if (otherFragment.isAbsolute()) {
- char driveLetter = getDriveLetter();
- return driveLetter == '\0' || otherFragment.getDriveLetter() != '\0'
- ? otherFragment
- : createAlreadyInterned(driveLetter, true, otherFragment.segments);
+ if (path.length() == bn) {
+ return EMPTY_FRAGMENT;
+ }
+ final int lastSlashIndex;
+ if (basePath.charAt(bn - 1) == '/') {
+ lastSlashIndex = bn - 1;
} else {
- return create(this, otherFragment);
+ lastSlashIndex = bn;
+ }
+ if (path.charAt(lastSlashIndex) != '/') {
+ throw new IllegalArgumentException(
+ String.format("Path '%s' is not under '%s', cannot relativize", this, base));
}
+ String newPath = path.substring(lastSlashIndex + 1);
+ return new PathFragment(newPath, 0 /* Always a relative path */);
}
- /**
- * 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));
+ public PathFragment relativeTo(String base) {
+ return relativeTo(PathFragment.create(base));
}
/**
- * Returns the path formed by appending the single non-special segment "baseName" to this path.
+ * Returns whether this path is an ancestor of another 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 '/'.
+ * <p>If this == other, true is returned.
*
- * @throws IllegalArgumentException if {@code baseName} is not a valid base name according to
- * {@link #checkBaseName}
+ * <p>An absolute path can never be an ancestor of a relative path, and vice versa.
*/
- 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);
+ public boolean startsWith(PathFragment other) {
+ Preconditions.checkNotNull(other);
+ if (other.path.length() > path.length()) {
+ return false;
+ }
+ if (driveStrLength != other.driveStrLength) {
+ return false;
+ }
+ if (!OS.startsWith(path, other.path)) {
+ return false;
+ }
+ return path.length() == other.path.length()
+ || other.path.length() == driveStrLength
+ || path.charAt(other.path.length()) == SEPARATOR_CHAR;
}
/**
- * Returns the last segment of this path, or "" for the empty fragment.
+ * 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 String getBaseName() {
- return (segments.length == 0) ? "" : segments[segments.length - 1];
+ public boolean endsWith(PathFragment other) {
+ Preconditions.checkNotNull(other);
+ if (other.path.length() > path.length()) {
+ return false;
+ }
+ if (other.isAbsolute()) {
+ return this.equals(other);
+ }
+ if (!OS.endsWith(path, other.path)) {
+ return false;
+ }
+ return path.length() == other.path.length()
+ || other.path.length() == 0
+ || path.charAt(path.length() - other.path.length() - 1) == SEPARATOR_CHAR;
}
- /**
- * Returns the file extension of this path, excluding the period, or "" if there is no extension.
- */
- public String getFileExtension() {
- String baseName = getBaseName();
+ public boolean isAbsolute() {
+ return driveStrLength > 0;
+ }
- int lastIndex = baseName.lastIndexOf('.');
- if (lastIndex != -1) {
- return baseName.substring(lastIndex + 1);
- }
+ public static boolean isAbsolute(String path) {
+ return OS.getDriveStrLength(path) > 0;
+ }
- return "";
+ @Override
+ public String toString() {
+ return path;
}
- /**
- * 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;
+ @Override
+ public void repr(SkylarkPrinter printer) {
+ printer.append(path);
+ }
- if (isAbsolute() != ancestorDirectory.isAbsolute() || segments.length < ancestorLength) {
- throw new IllegalArgumentException("PathFragment " + this
- + " is not beneath " + ancestorDirectory);
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
}
-
- if (!HELPER.segmentsEqual(ancestorLength, segments, 0, ancestorSegments)) {
- throw new IllegalArgumentException(
- "PathFragment " + this + " is not beneath " + ancestorDirectory);
+ if (o == null || getClass() != o.getClass()) {
+ return false;
}
-
- int length = segments.length - ancestorLength;
- String[] resultSegments = subarray(segments, ancestorLength, length);
- return createAlreadyInterned('\0', false, resultSegments);
+ return OS.equals(this.path, ((PathFragment) o).path);
}
- /**
- * Returns a relative path fragment to this path, relative to {@code path}.
- */
- public PathFragment relativeTo(String path) {
- return relativeTo(create(path));
+ @Override
+ public int hashCode() {
+ return OS.hash(this.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);
+ @Override
+ public int compareTo(PathFragment o) {
+ return OS.compare(this.path, o.path);
}
- /**
- * 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.
+ * Returns the number of segments in this path.
*
- * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
- * order)
+ * <p>This operation is O(N) on the length of the string.
*/
- public boolean startsWith(PathFragment prefix) {
- if (isAbsolute() != prefix.isAbsolute()
- || this.segments.length < prefix.segments.length
- || (isAbsolute() && getDriveLetter() != prefix.getDriveLetter())) {
- return false;
+ public int segmentCount() {
+ int n = path.length();
+ int segmentCount = 0;
+ int i;
+ for (i = driveStrLength; i < n; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ ++segmentCount;
+ }
}
- 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;
+ // Add last segment if one exists.
+ if (i > driveStrLength) {
+ ++segmentCount;
}
- 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;
+ return segmentCount;
}
/**
- * 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>.
+ * Returns the specified segment of this path; index must be positive and less than numSegments().
*
- * @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>.
+ * <p>This operation is O(N) on the length of the string.
*/
- 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));
+ public String getSegment(int index) {
+ int n = path.length();
+ int segmentCount = 0;
+ int i;
+ for (i = driveStrLength; i < n && segmentCount < index; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ ++segmentCount;
+ }
+ }
+ int starti = i;
+ for (; i < n; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ break;
+ }
}
- boolean isAbsolute = (beginIndex == 0) && isAbsolute();
- return ((beginIndex == 0) && (endIndex == count))
- ? this
- : createAlreadyInterned(
- getDriveLetter(), isAbsolute, subarray(segments, beginIndex, endIndex - beginIndex));
+ // Add last segment if one exists.
+ if (i > driveStrLength) {
+ ++segmentCount;
+ }
+ int endi = i;
+ if (index < 0 || index >= segmentCount) {
+ throw new IllegalArgumentException("Illegal segment index: " + index);
+ }
+ return path.substring(starti, endi);
}
/**
* 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.
+ * 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>.
+ *
+ * <p>This operation is O(N) on the length of the string.
*
* @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) {
- return subFragment(beginIndex, segments.length);
+ public PathFragment subFragment(int beginIndex, int endIndex) {
+ if (beginIndex < 0 || beginIndex > endIndex) {
+ throw new IndexOutOfBoundsException(
+ String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex));
+ }
+ return subFragmentImpl(beginIndex, endIndex);
}
- /**
- * 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 PathFragment subFragment(int beginIndex) {
+ if (beginIndex < 0) {
+ throw new IndexOutOfBoundsException(
+ String.format("path: %s, beginIndex: %d", toString(), beginIndex));
+ }
+ return subFragmentImpl(beginIndex, -1);
+ }
- public static boolean isAbsolute(String path) {
- return PathFragment.create(path).isAbsolute();
+ private PathFragment subFragmentImpl(int beginIndex, int endIndex) {
+ int n = path.length();
+ int segmentIndex = 0;
+ int i;
+ for (i = driveStrLength; i < n && segmentIndex < beginIndex; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ ++segmentIndex;
+ }
+ }
+ int starti = i;
+ if (segmentIndex < endIndex) {
+ for (; i < n; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ ++segmentIndex;
+ if (segmentIndex == endIndex) {
+ break;
+ }
+ }
+ }
+ } else if (endIndex == -1) {
+ i = path.length();
+ }
+ int endi = i;
+ // Add last segment if one exists for verification
+ if (i == n && i > driveStrLength) {
+ ++segmentIndex;
+ }
+ if (beginIndex > segmentIndex || endIndex > segmentIndex) {
+ throw new IndexOutOfBoundsException(
+ String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex));
+ }
+ // If beginIndex is 0 we include the drive. Very odd semantics.
+ int driveStrLength = 0;
+ if (beginIndex == 0) {
+ starti = 0;
+ driveStrLength = this.driveStrLength;
+ endi = Math.max(endi, driveStrLength);
+ }
+ return new PathFragment(path.substring(starti, endi), driveStrLength);
}
/**
@@ -647,62 +487,104 @@ public abstract class PathFragment
* modified.
*/
String[] segments() {
+ int segmentCount = segmentCount();
+ String[] segments = new String[segmentCount];
+ int segmentIndex = 0;
+ int nexti = driveStrLength;
+ int n = path.length();
+ for (int i = driveStrLength; i < n; ++i) {
+ if (path.charAt(i) == SEPARATOR_CHAR) {
+ segments[segmentIndex++] = path.substring(nexti, i);
+ nexti = i + 1;
+ }
+ }
+ // Add last segment if one exists.
+ if (nexti < n) {
+ segments[segmentIndex] = path.substring(nexti);
+ }
return segments;
}
+ /**
+ * Returns a list of the segments.
+ *
+ * <p>This operation is O(N) on the length of the string.
+ */
public ImmutableList<String> getSegments() {
- return ImmutableList.copyOf(segments);
+ 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 int getFirstSegment(Set<String> values) {
+ String[] segments = segments();
+ for (int i = 0; i < segments.length; ++i) {
+ if (values.contains(segments[i])) {
+ return i;
+ }
+ }
+ return INVALID_SEGMENT;
+ }
- public boolean isEmpty() {
- return segments.length == 0;
+ /** Returns the path string, or '.' if the path is empty. */
+ public String getSafePathString() {
+ return !path.isEmpty() ? path : ".";
}
/**
- * Returns the number of segments in this path.
+ * 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 int segmentCount() {
- return segments.length;
+ public String getCallablePathString() {
+ if (isAbsolute()) {
+ return path;
+ } else if (path.isEmpty()) {
+ return ".";
+ } else if (path.indexOf(SEPARATOR_CHAR) == -1) {
+ return "." + SEPARATOR_CHAR + path;
+ } else {
+ return path;
+ }
}
/**
- * Returns the specified segment of this path; index must be positive and
- * less than numSegments().
+ * Returns the file extension of this path, excluding the period, or "" if there is no extension.
*/
- public String getSegment(int index) {
- return segments[index];
+ public String getFileExtension() {
+ int n = path.length();
+ for (int i = n - 1; i > driveStrLength; --i) {
+ char c = path.charAt(i);
+ if (c == '.') {
+ return path.substring(i + 1, n);
+ } else if (c == SEPARATOR_CHAR) {
+ break;
+ }
+ }
+ return "";
}
/**
- * 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.
+ * 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 int getFirstSegment(Set<String> values) {
- for (int i = 0; i < segments.length; i++) {
- if (values.contains(segments[i])) {
- return i;
- }
- }
- return INVALID_SEGMENT;
+ public PathFragment replaceName(String newName) {
+ PathFragment parent = getParentDirectory();
+ return parent != null ? parent.getRelative(newName) : null;
}
/**
- * Returns true iff this path contains uplevel references "..".
+ * Returns the drive for an absolute path fragment.
+ *
+ * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/".
*/
- public boolean containsUplevelReferences() {
- for (String segment : segments) {
- if (segment.equals("..")) {
- return true;
- }
- }
- return false;
+ public String getDriveStr() {
+ Preconditions.checkArgument(isAbsolute());
+ return path.substring(0, driveStrLength);
}
/**
@@ -711,60 +593,129 @@ public abstract class PathFragment
*/
public PathFragment toRelative() {
Preconditions.checkArgument(isAbsolute());
- return HELPER.createAlreadyInterned(getDriveLetter(), false, segments);
+ return new PathFragment(path.substring(driveStrLength), 0);
}
- @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);
+ /**
+ * Returns true if this path contains uplevel references "..".
+ *
+ * <p>Since path fragments are normalized, this implies that the uplevel reference is at the start
+ * of the path fragment.
+ */
+ public boolean containsUplevelReferences() {
+ // Path is normalized, so any ".." would have to be at the very start
+ return path.startsWith("..");
+ }
/**
- * Compares two PathFragments using the lexicographical order.
+ * Returns true if the passed path contains uplevel references ".." or single-dot references "."
+ *
+ * <p>This is useful to check a string for normalization before constructing a PathFragment, since
+ * these are always normalized and will throw uplevel references away.
*/
- @Override
- public int compareTo(PathFragment p2) {
- return HELPER.compare(this, p2);
+ public static boolean isNormalized(String path) {
+ return isNormalizedImpl(path, true /* lookForSameLevelReferences */);
}
- @Override
- public String toString() {
- return getPathString();
+ /**
+ * Returns true if the passed path contains uplevel references "..".
+ *
+ * <p>This is useful to check a string for '..' segments before constructing a PathFragment, since
+ * these are always normalized and will throw uplevel references away.
+ */
+ public static boolean containsUplevelReferences(String path) {
+ return !isNormalizedImpl(path, false /* lookForSameLevelReferences */);
+ }
+
+ private enum NormalizedImplState {
+ Base, /* No particular state, eg. an 'a' or 'L' character */
+ Separator, /* We just saw a separator */
+ Dot, /* We just saw a dot after a separator */
+ DotDot, /* We just saw two dots after a separator */
+ }
+
+ private static boolean isNormalizedImpl(String path, boolean lookForSameLevelReferences) {
+ // Starting state is equivalent to having just seen a separator
+ NormalizedImplState state = NormalizedImplState.Separator;
+ int n = path.length();
+ for (int i = 0; i < n; ++i) {
+ char c = path.charAt(i);
+ boolean isSeparator = OS.isSeparator(c);
+ switch (state) {
+ case Base:
+ if (isSeparator) {
+ state = NormalizedImplState.Separator;
+ } else {
+ state = NormalizedImplState.Base;
+ }
+ break;
+ case Separator:
+ if (isSeparator) {
+ state = NormalizedImplState.Separator;
+ } else if (c == '.') {
+ state = NormalizedImplState.Dot;
+ } else {
+ state = NormalizedImplState.Base;
+ }
+ break;
+ case Dot:
+ if (isSeparator) {
+ if (lookForSameLevelReferences) {
+ // "." segment found
+ return false;
+ }
+ state = NormalizedImplState.Separator;
+ } else if (c == '.') {
+ state = NormalizedImplState.DotDot;
+ } else {
+ state = NormalizedImplState.Base;
+ }
+ break;
+ case DotDot:
+ if (isSeparator) {
+ // ".." segment found
+ return false;
+ } else {
+ state = NormalizedImplState.Base;
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unhandled state: " + state);
+ }
+ }
+ // The character just after the string is equivalent to a separator
+ switch (state) {
+ case Dot:
+ if (lookForSameLevelReferences) {
+ // "." segment found
+ return false;
+ }
+ break;
+ case DotDot:
+ return false;
+ default:
+ }
+ return true;
}
- @Override
- public void repr(SkylarkPrinter printer) {
- printer.append(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);
+ }
}
@Override
public String filePathForFileTypeMatcher() {
- return getBaseName();
+ return path;
}
@Override
@@ -783,25 +734,13 @@ public abstract class PathFragment
@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);
- }
+ stringCodec.serialize(pathFragment.getPathString(), 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);
+ return PathFragment.createAlreadyNormalized(stringCodec.deserialize(codedIn));
}
}
@@ -816,4 +755,8 @@ public abstract class PathFragment
throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'");
}
}
+
+ private Object writeReplace() {
+ return new PathFragmentSerializationProxy(path);
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
index 39aefd1318..4ba66be7fc 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
@@ -17,7 +17,6 @@ import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectOutput;
-
/**
* A helper proxy for serializing immutable {@link PathFragment} objects.
*/
@@ -44,7 +43,7 @@ public final class PathFragmentSerializationProxy implements Externalizable {
}
private Object readResolve() {
- return PathFragment.create(pathFragmentString);
+ return PathFragment.createAlreadyNormalized(pathFragmentString);
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java b/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java
deleted file mode 100644
index fd783c5929..0000000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// 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.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A trie that operates on path segments.
- *
- * @param <T> the type of the values.
- */
-@ThreadCompatible
-public class PathTrie<T> {
- @SuppressWarnings("unchecked")
- private static class Node<T> {
- private Node() {
- children = new HashMap<>();
- }
-
- private T value;
- private Map<String, Node<T>> children;
- }
-
- private final Node<T> root;
-
- public PathTrie() {
- root = new Node<T>();
- }
-
- /**
- * Puts a value in the trie.
- *
- * @param key must be an absolute path.
- */
- public void put(PathFragment key, T value) {
- Preconditions.checkArgument(key.isAbsolute(), "PathTrie only accepts absolute paths as keys.");
- Node<T> current = root;
- for (String segment : key.getSegments()) {
- current.children.putIfAbsent(segment, new Node<T>());
- current = current.children.get(segment);
- }
- current.value = value;
- }
-
- /**
- * Gets a value from the trie. If there is an entry with the same key, that will be returned,
- * otherwise, the value corresponding to the key that matches the longest prefix of the input.
- */
- public T get(PathFragment key) {
- Node<T> current = root;
- T lastValue = current.value;
-
- for (String segment : key.getSegments()) {
- if (current.children.containsKey(segment)) {
- current = current.children.get(segment);
- // Track the values of increasing matching prefixes.
- if (current.value != null) {
- lastValue = current.value;
- }
- } else {
- // We've reached the longest prefix, no further to go.
- break;
- }
- }
-
- return lastValue;
- }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
index 9182f10c0f..09ab3139a4 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
@@ -30,9 +30,8 @@ import java.util.Objects;
* <p>Two {@link RootedPath}s are considered equal iff they have equal roots and equal relative
* paths.
*
- * <p>TODO(bazel-team): refactor Artifact to use this instead of Root. TODO(bazel-team): use an
- * opaque root representation so as to not expose the absolute path to clients via #asPath or
- * #getRoot.
+ * <p>TODO(bazel-team): use an opaque root representation so as to not expose the absolute path to
+ * clients via #asPath or #getRoot.
*/
public class RootedPath implements Serializable {
@@ -48,7 +47,7 @@ public class RootedPath implements Serializable {
rootRelativePath,
root);
this.root = root;
- this.rootRelativePath = rootRelativePath.normalize();
+ this.rootRelativePath = rootRelativePath;
this.path = root.getRelative(this.rootRelativePath);
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
index d924e19d45..a929f1435d 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
@@ -15,12 +15,14 @@
package com.google.devtools.build.lib.vfs;
import com.google.common.base.Preconditions;
-import com.google.common.collect.Lists;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -42,12 +44,21 @@ import javax.annotation.Nullable;
* are not currently supported.
*/
@ThreadSafety.ThreadSafe
-public class UnionFileSystem extends FileSystem {
+public final class UnionFileSystem extends FileSystem {
- // Prefix trie index, allowing children to easily inherit prefix mappings
- // of their parents.
- // This does not currently handle unicode filenames.
- private final PathTrie<FileSystem> pathDelegate;
+ private static class FileSystemAndPrefix {
+ final PathFragment prefix;
+ final FileSystem fileSystem;
+
+ public FileSystemAndPrefix(PathFragment prefix, FileSystem fileSystem) {
+ this.prefix = prefix;
+ this.fileSystem = fileSystem;
+ }
+ }
+
+ // List of file systems and their mappings, sorted by prefix length descending.
+ private final List<FileSystemAndPrefix> fileSystems;
+ private final FileSystem rootFileSystem;
// True if the file path is case-sensitive on all the FileSystem
// or False if they are all case-insensitive, otherwise error.
@@ -65,11 +76,12 @@ public class UnionFileSystem extends FileSystem {
Preconditions.checkNotNull(rootFileSystem);
Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
Preconditions.checkArgument(
- !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
+ prefixMapping.keySet().stream().noneMatch(p -> p.getPathString().equals("/")),
"Attempted to specify an explicit root prefix mapping; "
+ "please use the rootFileSystem argument instead.");
- this.pathDelegate = new PathTrie<>();
+ this.fileSystems = new ArrayList<>();
+ this.rootFileSystem = rootFileSystem;
this.isCaseSensitive = rootFileSystem.isFilePathCaseSensitive();
for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
@@ -80,9 +92,13 @@ public class UnionFileSystem extends FileSystem {
PathFragment prefixPath = prefix.getKey();
// Extra slash prevents within-directory mappings, which Path can't handle.
- pathDelegate.put(prefixPath, delegate);
+ fileSystems.add(new FileSystemAndPrefix(prefixPath, delegate));
}
- pathDelegate.put(PathFragment.ROOT_FRAGMENT, rootFileSystem);
+ // Order by length descending. This ensures that more specific mapping takes precedence
+ // when we try to find the file system of a given path.
+ Comparator<FileSystemAndPrefix> comparator =
+ Comparator.comparing(f -> f.prefix.getPathString().length());
+ fileSystems.sort(comparator.reversed());
}
/**
@@ -92,19 +108,24 @@ public class UnionFileSystem extends FileSystem {
* @param path the {@link Path} to map to a filesystem
* @throws IllegalArgumentException if no delegate exists for the path
*/
- protected FileSystem getDelegate(Path path) {
+ FileSystem getDelegate(Path path) {
Preconditions.checkNotNull(path);
- FileSystem immediateDelegate = pathDelegate.get(path.asFragment());
-
- // Should never actually happen if the root delegate is present.
- Preconditions.checkNotNull(immediateDelegate, "No delegate filesystem exists for %s", path);
- return immediateDelegate;
+ FileSystem delegate = null;
+ // Linearly iterate over each mapped file system and find the one that handles this path.
+ // For small number of mappings, this will be more efficient than using a trie
+ for (FileSystemAndPrefix fileSystemAndPrefix : this.fileSystems) {
+ if (path.startsWith(fileSystemAndPrefix.prefix)) {
+ delegate = fileSystemAndPrefix.fileSystem;
+ break;
+ }
+ }
+ return delegate != null ? delegate : rootFileSystem;
}
// Associates the path with the root of the given delegate filesystem.
// Necessary to avoid null pointer problems inside of the delegates.
- protected Path adjustPath(Path path, FileSystem delegate) {
- return delegate.getPath(path.asFragment());
+ Path adjustPath(Path path, FileSystem delegate) {
+ return delegate.getPath(path.getPathString());
}
/**
@@ -344,12 +365,7 @@ public class UnionFileSystem extends FileSystem {
path = internalResolveSymlink(path);
FileSystem delegate = getDelegate(path);
Path resolvedPath = adjustPath(path, delegate);
- Collection<Path> entries = resolvedPath.getDirectoryEntries();
- Collection<String> result = Lists.newArrayListWithCapacity(entries.size());
- for (Path entry : entries) {
- result.add(entry.getBaseName());
- }
- return result;
+ return delegate.getDirectoryEntries(resolvedPath);
}
// No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java
new file mode 100644
index 0000000000..8576643267
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java
@@ -0,0 +1,138 @@
+// Copyright 2017 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.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+@VisibleForTesting
+class UnixOsPathPolicy implements OsPathPolicy {
+
+ static final UnixOsPathPolicy INSTANCE = new UnixOsPathPolicy();
+ private static final Splitter PATH_SPLITTER = Splitter.onPattern("/+").omitEmptyStrings();
+
+ @Override
+ public int needsToNormalize(String path) {
+ int n = path.length();
+ int dotCount = 0;
+ char prevChar = 0;
+ for (int i = 0; i < n; i++) {
+ char c = path.charAt(i);
+ if (c == '\\') {
+ return NEEDS_NORMALIZE;
+ }
+ if (c == '/') {
+ if (prevChar == '/') {
+ return NEEDS_NORMALIZE;
+ }
+ if (dotCount == 1 || dotCount == 2) {
+ return NEEDS_NORMALIZE;
+ }
+ }
+ dotCount = c == '.' ? dotCount + 1 : 0;
+ prevChar = c;
+ }
+ if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
+ return NEEDS_NORMALIZE;
+ }
+ return NORMALIZED;
+ }
+
+ @Override
+ public int needsToNormalizeSuffix(String normalizedSuffix) {
+ // We know that the string is normalized
+ // In this case only suffixes starting with ".." may cause
+ // normalization once concatenated with other strings
+ return normalizedSuffix.startsWith("..") ? NEEDS_NORMALIZE : NORMALIZED;
+ }
+
+ @Override
+ public String normalize(String path, int normalizationLevel) {
+ if (normalizationLevel == NORMALIZED) {
+ return path;
+ }
+ if (path.isEmpty()) {
+ return path;
+ }
+ boolean isAbsolute = path.charAt(0) == '/';
+ StringBuilder sb = new StringBuilder(path.length());
+ if (isAbsolute) {
+ sb.append('/');
+ }
+ String[] segments = Iterables.toArray(PATH_SPLITTER.splitToList(path), String.class);
+ int segmentCount = Utils.removeRelativePaths(segments, 0, isAbsolute);
+ for (int i = 0; i < segmentCount; ++i) {
+ sb.append(segments[i]);
+ sb.append('/');
+ }
+ if (segmentCount > 0) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int getDriveStrLength(String path) {
+ if (path.length() == 0) {
+ return 0;
+ }
+ return (path.charAt(0) == '/') ? 1 : 0;
+ }
+
+ @Override
+ public int compare(String s1, String s2) {
+ return s1.compareTo(s2);
+ }
+
+ @Override
+ public int compare(char c1, char c2) {
+ return Character.compare(c1, c2);
+ }
+
+ @Override
+ public boolean equals(String s1, String s2) {
+ return s1.equals(s2);
+ }
+
+ @Override
+ public int hash(String s) {
+ return s.hashCode();
+ }
+
+ @Override
+ public boolean startsWith(String path, String prefix) {
+ return path.startsWith(prefix);
+ }
+
+ @Override
+ public boolean endsWith(String path, String suffix) {
+ return path.endsWith(suffix);
+ }
+
+ @Override
+ public char getSeparator() {
+ return '/';
+ }
+
+ @Override
+ public boolean isSeparator(char c) {
+ return c == '/';
+ }
+
+ @Override
+ public boolean isCaseSensitive() {
+ return true;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java
deleted file mode 100644
index b581d4375a..0000000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java
+++ /dev/null
@@ -1,220 +0,0 @@
-// Copyright 2017 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 java.io.InvalidObjectException;
-import java.io.ObjectInputStream;
-
-/**
- * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run
- * on a non-Windows platform.
- */
-abstract class UnixPathFragment extends PathFragment {
- static final Helper HELPER = new Helper();
- /**
- * We have two concrete subclasses with zero per-instance additional memory overhead. Do not add
- * any fields. See the comment on memory use in PathFragment for details on the current
- * per-instance memory usage.
- */
-
- protected UnixPathFragment(String[] segments) {
- super(segments);
- }
-
- @Override
- protected int computeHashCode() {
- int h = 0;
- for (String segment : segments) {
- int segmentHash = segment.hashCode();
- h = h * 31 + segmentHash;
- }
- return h;
- }
-
- @Override
- public String windowsVolume() {
- return "";
- }
-
- @Override
- public char getDriveLetter() {
- return '\0';
- }
-
- private static class Helper extends PathFragment.Helper {
- private static final char SEPARATOR_CHAR = '/';
-
- @Override
- PathFragment create(String path) {
- boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
- return isAbsolute
- ? new AbsoluteUnixPathFragment(segment(path, 1))
- : new RelativeUnixPathFragment(segment(path, 0));
- }
-
- @Override
- PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) {
- Preconditions.checkState(driveLetter == '\0', driveLetter);
- return isAbsolute
- ? new AbsoluteUnixPathFragment(segments)
- : new RelativeUnixPathFragment(segments);
- }
-
- @Override
- char getPrimarySeparatorChar() {
- return SEPARATOR_CHAR;
- }
-
- @Override
- boolean isSeparator(char c) {
- return c == SEPARATOR_CHAR;
- }
-
- @Override
- boolean containsSeparatorChar(String path) {
- return path.indexOf(SEPARATOR_CHAR) != -1;
- }
-
- @Override
- boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) {
- if ((segments1.length - offset1) < length || segments2.length < length) {
- return false;
- }
- for (int i = 0; i < length; ++i) {
- String seg1 = segments1[i + offset1];
- String seg2 = segments2[i];
- if ((seg1 == null) != (seg2 == null)) {
- return false;
- }
- if (seg1 == null) {
- continue;
- }
- if (!seg1.equals(seg2)) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) {
- if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) {
- return pathFragment1.isAbsolute() ? -1 : 1;
- }
- String[] segments1 = pathFragment1.segments();
- String[] segments2 = pathFragment2.segments();
- int len1 = segments1.length;
- int len2 = segments2.length;
- int n = Math.min(len1, len2);
- for (int i = 0; i < n; i++) {
- String seg1 = segments1[i];
- String seg2 = segments2[i];
- int cmp = seg1.compareTo(seg2);
- if (cmp != 0) {
- return cmp;
- }
- }
- return len1 - len2;
- }
- }
-
- private static final class AbsoluteUnixPathFragment extends UnixPathFragment {
- private AbsoluteUnixPathFragment(String[] segments) {
- super(segments);
- }
-
- @Override
- public boolean isAbsolute() {
- return true;
- }
-
- @Override
- protected int computeHashCode() {
- int h = Boolean.TRUE.hashCode();
- h = h * 31 + super.computeHashCode();
- return h;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof AbsoluteUnixPathFragment)) {
- return false;
- }
- if (this == other) {
- return true;
- }
- AbsoluteUnixPathFragment otherAbsoluteUnixPathFragment = (AbsoluteUnixPathFragment) other;
- return HELPER.segmentsEqual(this.segments, otherAbsoluteUnixPathFragment.segments);
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected Object writeReplace() {
- return super.writeReplace();
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
- super.readObject(stream);
- }
- }
-
- private static final class RelativeUnixPathFragment extends UnixPathFragment {
- private RelativeUnixPathFragment(String[] segments) {
- super(segments);
- }
-
- @Override
- public boolean isAbsolute() {
- return false;
- }
-
- @Override
- protected int computeHashCode() {
- int h = Boolean.FALSE.hashCode();
- h = h * 31 + super.computeHashCode();
- return h;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativeUnixPathFragment)) {
- return false;
- }
- if (this == other) {
- return true;
- }
- RelativeUnixPathFragment otherRelativeUnixPathFragment = (RelativeUnixPathFragment) other;
- return HELPER.segmentsEqual(this.segments, otherRelativeUnixPathFragment.segments);
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected Object writeReplace() {
- return super.writeReplace();
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
- super.readObject(stream);
- }
- }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java
new file mode 100644
index 0000000000..f420683b75
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java
@@ -0,0 +1,240 @@
+// Copyright 2017 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.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.windows.WindowsShortPath;
+import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
+import java.io.IOException;
+
+@VisibleForTesting
+class WindowsOsPathPolicy implements OsPathPolicy {
+
+ static final WindowsOsPathPolicy INSTANCE = new WindowsOsPathPolicy();
+
+ static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1;
+
+ private static final Splitter WINDOWS_PATH_SPLITTER =
+ Splitter.onPattern("[\\\\/]+").omitEmptyStrings();
+
+ private final ShortPathResolver shortPathResolver;
+
+ interface ShortPathResolver {
+ String resolveShortPath(String path);
+ }
+
+ static class DefaultShortPathResolver implements ShortPathResolver {
+ @Override
+ public String resolveShortPath(String path) {
+ try {
+ return WindowsFileOperations.getLongPath(path);
+ } catch (IOException e) {
+ return path;
+ }
+ }
+ }
+
+ WindowsOsPathPolicy() {
+ this(new DefaultShortPathResolver());
+ }
+
+ WindowsOsPathPolicy(ShortPathResolver shortPathResolver) {
+ this.shortPathResolver = shortPathResolver;
+ }
+
+ @Override
+ public int needsToNormalize(String path) {
+ int n = path.length();
+ int normalizationLevel = NORMALIZED;
+ int dotCount = 0;
+ char prevChar = 0;
+ int segmentBeginIndex = 0; // The start index of the current path index
+ boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test
+ for (int i = 0; i < n; i++) {
+ char c = path.charAt(i);
+ if (isSeparator(c)) {
+ if (c == '\\') {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+ }
+ // No need to check for '\\' here because that already causes normalization
+ if (prevChar == '/') {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+ }
+ if (dotCount == 1 || dotCount == 2) {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+ }
+ if (segmentHasShortPathChar) {
+ if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
+ }
+ }
+ segmentBeginIndex = i + 1;
+ segmentHasShortPathChar = false;
+ } else if (c == '~') {
+ // This path segment might be a Windows short path segment
+ segmentHasShortPathChar = true;
+ }
+ dotCount = c == '.' ? dotCount + 1 : 0;
+ prevChar = c;
+ }
+ if (segmentHasShortPathChar) {
+ if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex))) {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
+ }
+ }
+ if ((n > 1 && isSeparator(prevChar)) || dotCount == 1 || dotCount == 2) {
+ normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+ }
+ return normalizationLevel;
+ }
+
+ @Override
+ public int needsToNormalizeSuffix(String normalizedSuffix) {
+ // On Windows, all bets are off because of short paths, so we have to check the entire string
+ return needsToNormalize(normalizedSuffix);
+ }
+
+ @Override
+ public String normalize(String path, int normalizationLevel) {
+ if (normalizationLevel == NORMALIZED) {
+ return path;
+ }
+ if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) {
+ String resolvedPath = shortPathResolver.resolveShortPath(path);
+ if (resolvedPath != null) {
+ path = resolvedPath;
+ }
+ }
+ String[] segments = Iterables.toArray(WINDOWS_PATH_SPLITTER.splitToList(path), String.class);
+ int driveStrLength = getDriveStrLength(path);
+ boolean isAbsolute = driveStrLength > 0;
+ int segmentSkipCount = isAbsolute && driveStrLength > 1 ? 1 : 0;
+
+ StringBuilder sb = new StringBuilder(path.length());
+ if (isAbsolute) {
+ char c = path.charAt(0);
+ if (isSeparator(c)) {
+ sb.append('/');
+ } else {
+ sb.append(Character.toUpperCase(c));
+ sb.append(":/");
+ }
+ }
+ int segmentCount = Utils.removeRelativePaths(segments, segmentSkipCount, isAbsolute);
+ for (int i = 0; i < segmentCount; ++i) {
+ sb.append(segments[i]);
+ sb.append('/');
+ }
+ if (segmentCount > 0) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int getDriveStrLength(String path) {
+ int n = path.length();
+ if (n == 0) {
+ return 0;
+ }
+ char c0 = path.charAt(0);
+ if (isSeparator(c0)) {
+ return 1;
+ }
+ if (n < 3) {
+ return 0;
+ }
+ char c1 = path.charAt(1);
+ char c2 = path.charAt(2);
+ if (isDriveLetter(c0) && c1 == ':' && isSeparator(c2)) {
+ return 3;
+ }
+ return 0;
+ }
+
+ private static boolean isDriveLetter(char c) {
+ return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
+ }
+
+ @Override
+ public int compare(String s1, String s2) {
+ // Windows is case-insensitive
+ return s1.compareToIgnoreCase(s2);
+ }
+
+ @Override
+ public int compare(char c1, char c2) {
+ return Character.compare(Character.toLowerCase(c1), Character.toLowerCase(c2));
+ }
+
+ @Override
+ public boolean equals(String s1, String s2) {
+ return s1.equalsIgnoreCase(s2);
+ }
+
+ @Override
+ public int hash(String s) {
+ // Windows is case-insensitive
+ return s.toLowerCase().hashCode();
+ }
+
+ @Override
+ public boolean startsWith(String path, String prefix) {
+ int pathn = path.length();
+ int prefixn = prefix.length();
+ if (pathn < prefixn) {
+ return false;
+ }
+ for (int i = 0; i < prefixn; ++i) {
+ if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean endsWith(String path, String suffix) {
+ int pathn = path.length();
+ int suffixLength = suffix.length();
+ if (pathn < suffixLength) {
+ return false;
+ }
+ int offset = pathn - suffixLength;
+ for (int i = 0; i < suffixLength; ++i) {
+ if (Character.toLowerCase(path.charAt(i + offset))
+ != Character.toLowerCase(suffix.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public char getSeparator() {
+ return '/';
+ }
+
+ @Override
+ public boolean isSeparator(char c) {
+ return c == '/' || c == '\\';
+ }
+
+ @Override
+ public boolean isCaseSensitive() {
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java
deleted file mode 100644
index ac3be0f524..0000000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright 2017 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 java.io.InvalidObjectException;
-import java.io.ObjectInputStream;
-
-/**
- * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run
- * on a Windows platform.
- */
-abstract class WindowsPathFragment extends PathFragment {
- static final Helper HELPER = new Helper();
-
- // The drive letter of an absolute path, eg. 'C' for 'C:/foo'.
- // We deliberately ignore "C:foo" style paths and treat them like a literal "C:foo" path segment.
- protected final char driveLetter;
-
- protected WindowsPathFragment(char driveLetter, String[] segments) {
- super(segments);
- this.driveLetter = driveLetter;
- }
-
- @Override
- public String windowsVolume() {
- return (driveLetter != '\0') ? driveLetter + ":" : "";
- }
-
- @Override
- public char getDriveLetter() {
- return driveLetter;
- }
-
- @Override
- protected int computeHashCode() {
- int h = 0;
- for (String segment : segments) {
- int segmentHash = segment.toLowerCase().hashCode();
- h = h * 31 + segmentHash;
- }
- return h;
- }
-
- private static class Helper extends PathFragment.Helper {
- private static final char SEPARATOR_CHAR = '/';
- // TODO(laszlocsomor): Lots of internal PathFragment operations, e.g. getPathString, use the
- // primary separator char and do not use this.
- private static final char EXTRA_SEPARATOR_CHAR = '\\';
-
- private static boolean isDriveLetter(char c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
- }
-
- @Override
- PathFragment create(String path) {
- char driveLetter =
- path.length() >= 3
- && path.charAt(1) == ':'
- && isSeparator(path.charAt(2))
- && isDriveLetter(path.charAt(0))
- ? Character.toUpperCase(path.charAt(0))
- : '\0';
- if (driveLetter != '\0') {
- path = path.substring(2);
- }
- boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
- return isAbsolute
- ? new AbsoluteWindowsPathFragment(driveLetter, segment(path, 1))
- : new RelativeWindowsPathFragment(driveLetter, segment(path, 0));
- }
-
- @Override
- PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) {
- return isAbsolute
- ? new AbsoluteWindowsPathFragment(driveLetter, segments)
- : new RelativeWindowsPathFragment(driveLetter, segments);
- }
-
- @Override
- char getPrimarySeparatorChar() {
- return SEPARATOR_CHAR;
- }
-
- @Override
- boolean isSeparator(char c) {
- return c == SEPARATOR_CHAR || c == EXTRA_SEPARATOR_CHAR;
- }
-
- @Override
- boolean containsSeparatorChar(String path) {
- // TODO(laszlocsomor): This is inefficient.
- return path.indexOf(SEPARATOR_CHAR) != -1 || path.indexOf(EXTRA_SEPARATOR_CHAR) != -1;
- }
-
- @Override
- boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) {
- if ((segments1.length - offset1) < length || segments2.length < length) {
- return false;
- }
- for (int i = 0; i < length; ++i) {
- String seg1 = segments1[i + offset1];
- String seg2 = segments2[i];
- if ((seg1 == null) != (seg2 == null)) {
- return false;
- }
- if (seg1 == null) {
- continue;
- }
- // TODO(laszlocsomor): The calls to String#toLowerCase are inefficient and potentially
- // repeated too. Also, why not use String#equalsIgnoreCase.
- seg1 = seg1.toLowerCase();
- seg2 = seg2.toLowerCase();
- if (!seg1.equals(seg2)) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) {
- if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) {
- return pathFragment1.isAbsolute() ? -1 : 1;
- }
- int cmp = Character.compare(pathFragment1.getDriveLetter(), pathFragment2.getDriveLetter());
- if (cmp != 0) {
- return cmp;
- }
- String[] segments1 = pathFragment1.segments();
- String[] segments2 = pathFragment2.segments();
- int len1 = segments1.length;
- int len2 = segments2.length;
- int n = Math.min(len1, len2);
- for (int i = 0; i < n; i++) {
- String seg1 = segments1[i].toLowerCase();
- String seg2 = segments2[i].toLowerCase();
- cmp = seg1.compareTo(seg2);
- if (cmp != 0) {
- return cmp;
- }
- }
- return len1 - len2;
- }
- }
-
- private static final class AbsoluteWindowsPathFragment extends WindowsPathFragment {
- private AbsoluteWindowsPathFragment(char driveLetter, String[] segments) {
- super(driveLetter, segments);
- }
-
- @Override
- public boolean isAbsolute() {
- return true;
- }
-
- @Override
- protected int computeHashCode() {
- int h = Boolean.TRUE.hashCode();
- h = h * 31 + super.computeHashCode();
- h = h * 31 + Character.valueOf(getDriveLetter()).hashCode();
- return h;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof AbsoluteWindowsPathFragment)) {
- return false;
- }
- if (this == other) {
- return true;
- }
- AbsoluteWindowsPathFragment otherAbsoluteWindowsPathFragment =
- (AbsoluteWindowsPathFragment) other;
- return this.driveLetter == otherAbsoluteWindowsPathFragment.driveLetter
- && HELPER.segmentsEqual(this.segments, otherAbsoluteWindowsPathFragment.segments);
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected Object writeReplace() {
- return super.writeReplace();
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
- super.readObject(stream);
- }
- }
-
- private static final class RelativeWindowsPathFragment extends WindowsPathFragment {
- private RelativeWindowsPathFragment(char driveLetter, String[] segments) {
- super(driveLetter, segments);
- }
-
- @Override
- public boolean isAbsolute() {
- return false;
- }
-
- @Override
- protected int computeHashCode() {
- int h = Boolean.FALSE.hashCode();
- h = h * 31 + super.computeHashCode();
- if (!isEmpty()) {
- h = h * 31 + Character.valueOf(getDriveLetter()).hashCode();
- }
- return h;
- }
-
- @Override
- public boolean equals(Object other) {
- if (!(other instanceof RelativeWindowsPathFragment)) {
- return false;
- }
- if (this == other) {
- return true;
- }
- RelativeWindowsPathFragment otherRelativeWindowsPathFragment =
- (RelativeWindowsPathFragment) other;
- return isEmpty() && otherRelativeWindowsPathFragment.isEmpty()
- ? true
- : this.driveLetter == otherRelativeWindowsPathFragment.driveLetter
- && HELPER.segmentsEqual(this.segments, otherRelativeWindowsPathFragment.segments);
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected Object writeReplace() {
- return super.writeReplace();
- }
-
- // Java serialization looks for the presence of this method in the concrete class. It is not
- // inherited from the parent class.
- @Override
- protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
- super.readObject(stream);
- }
- }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
index 8fd21180b4..c2a52a5b3d 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -350,7 +350,8 @@ public class InMemoryFileSystem extends FileSystem {
Stack<String> stack = new Stack<>();
for (Path p = path; !isRootDirectory(p); p = p.getParentDirectory()) {
- stack.push(p.getBaseName());
+ String name = baseNameOrWindowsDrive(p);
+ stack.push(name);
}
InMemoryContentInfo inode = rootInode;
@@ -381,6 +382,13 @@ public class InMemoryFileSystem extends FileSystem {
for (int ii = segments.size() - 1; ii >= 0; --ii) {
stack.push(segments.get(ii)); // Note this may include ".." segments.
}
+ // Push Windows drive if there is one
+ if (linkTarget.isAbsolute()) {
+ String driveStr = linkTarget.getDriveStr();
+ if (driveStr.length() > 1) {
+ stack.push(driveStr);
+ }
+ }
} else {
inode = child;
}
@@ -412,7 +420,7 @@ public class InMemoryFileSystem extends FileSystem {
private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path)
throws IOException {
InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory());
- return directoryLookup(dirInfo, path.getBaseName(), /*create=*/ false, path);
+ return directoryLookup(dirInfo, baseNameOrWindowsDrive(path), /*create=*/ false, path);
}
/**
@@ -606,7 +614,7 @@ public class InMemoryFileSystem extends FileSystem {
InMemoryDirectoryInfo parent;
synchronized (this) {
parent = getDirectory(path.getParentDirectory());
- InMemoryContentInfo child = parent.getChild(path.getBaseName());
+ InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path));
if (child != null) { // already exists
if (child.isDirectory()) {
return false;
@@ -618,7 +626,7 @@ public class InMemoryFileSystem extends FileSystem {
InMemoryDirectoryInfo newDir = new InMemoryDirectoryInfo(clock);
newDir.addChild(".", newDir);
newDir.addChild("..", parent);
- insert(parent, path.getBaseName(), newDir, path);
+ insert(parent, baseNameOrWindowsDrive(path), newDir, path);
return true;
}
@@ -649,10 +657,11 @@ public class InMemoryFileSystem extends FileSystem {
synchronized (this) {
InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory());
- if (parent.getChild(path.getBaseName()) != null) {
+ if (parent.getChild(baseNameOrWindowsDrive(path)) != null) {
throw Error.EEXIST.exception(path);
}
- insert(parent, path.getBaseName(), new InMemoryLinkInfo(clock, targetFragment), path);
+ insert(
+ parent, baseNameOrWindowsDrive(path), new InMemoryLinkInfo(clock, targetFragment), path);
}
}
@@ -703,11 +712,11 @@ public class InMemoryFileSystem extends FileSystem {
synchronized (this) {
InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory());
- InMemoryContentInfo child = parent.getChild(path.getBaseName());
+ InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path));
if (child.isDirectory() && child.getSize() > 2) {
throw Error.ENOTEMPTY.exception(path);
}
- unlink(parent, path.getBaseName(), path);
+ unlink(parent, baseNameOrWindowsDrive(path), path);
return true;
}
}
@@ -780,13 +789,13 @@ public class InMemoryFileSystem extends FileSystem {
InMemoryDirectoryInfo sourceParent = getDirectory(sourcePath.getParentDirectory());
InMemoryDirectoryInfo targetParent = getDirectory(targetPath.getParentDirectory());
- InMemoryContentInfo sourceInode = sourceParent.getChild(sourcePath.getBaseName());
+ InMemoryContentInfo sourceInode = sourceParent.getChild(baseNameOrWindowsDrive(sourcePath));
if (sourceInode == null) {
throw Error.ENOENT.exception(sourcePath);
}
- InMemoryContentInfo targetInode = targetParent.getChild(targetPath.getBaseName());
+ InMemoryContentInfo targetInode = targetParent.getChild(baseNameOrWindowsDrive(targetPath));
- unlink(sourceParent, sourcePath.getBaseName(), sourcePath);
+ unlink(sourceParent, baseNameOrWindowsDrive(sourcePath), sourcePath);
try {
// TODO(bazel-team): (2009) test with symbolic links.
@@ -802,15 +811,19 @@ public class InMemoryFileSystem extends FileSystem {
} else if (sourceInode.isDirectory()) {
throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.ENOTDIR + ")");
}
- unlink(targetParent, targetPath.getBaseName(), targetPath);
+ unlink(targetParent, baseNameOrWindowsDrive(targetPath), targetPath);
}
sourceInode.movedTo(targetPath);
- insert(targetParent, targetPath.getBaseName(), sourceInode, targetPath);
+ insert(targetParent, baseNameOrWindowsDrive(targetPath), sourceInode, targetPath);
return;
} catch (IOException e) {
sourceInode.movedTo(sourcePath);
- insert(sourceParent, sourcePath.getBaseName(), sourceInode, sourcePath); // restore source
+ insert(
+ sourceParent,
+ baseNameOrWindowsDrive(sourcePath),
+ sourceInode,
+ sourcePath); // restore source
throw e;
}
}
@@ -828,18 +841,33 @@ public class InMemoryFileSystem extends FileSystem {
synchronized (this) {
InMemoryDirectoryInfo linkParent = getDirectory(linkPath.getParentDirectory());
// Same check used when creating a symbolic link
- if (linkParent.getChild(linkPath.getBaseName()) != null) {
+ if (linkParent.getChild(baseNameOrWindowsDrive(linkPath)) != null) {
throw Error.EEXIST.exception(linkPath);
}
insert(
linkParent,
- linkPath.getBaseName(),
- getDirectory(originalPath.getParentDirectory()).getChild(originalPath.getBaseName()),
+ baseNameOrWindowsDrive(linkPath),
+ getDirectory(originalPath.getParentDirectory())
+ .getChild(baseNameOrWindowsDrive(originalPath)),
linkPath);
}
}
- private boolean isRootDirectory(Path path) {
- return path.isRootDirectory();
+ /**
+ * On Unix the root directory is "/". On Windows there isn't one, so we reach null from
+ * getParentDirectory.
+ */
+ private boolean isRootDirectory(@Nullable Path path) {
+ return path == null || path.getPathString().equals("/");
+ }
+
+ /**
+ * Returns either the base name of the path, or the drive (if referring to a Windows drive).
+ *
+ * <p>This allows the file system to treat windows drives much like directories.
+ */
+ private static String baseNameOrWindowsDrive(Path path) {
+ String name = path.getBaseName();
+ return !name.isEmpty() ? name : path.getDriveStr();
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
index 107f319115..dabbbcefeb 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
@@ -31,7 +31,7 @@ class InMemoryLinkInfo extends InMemoryContentInfo {
InMemoryLinkInfo(Clock clock, PathFragment linkContent) {
super(clock);
this.linkContent = linkContent;
- this.normalizedLinkContent = linkContent.normalize();
+ this.normalizedLinkContent = linkContent;
}
@Override
@@ -56,7 +56,7 @@ class InMemoryLinkInfo extends InMemoryContentInfo {
@Override
public long getSize() {
- return linkContent.toString().length();
+ return linkContent.getSafePathString().length();
}
/**