VFS: implement a Windows-specific Path subclass
The new subclass WindowsFileSystem.WindowsPath is
aware of Windows drives.
This change:
- introduces a new factory for Path objects so
FileSystems can return a custom implementation
that instantiates filesystem-specific Paths
- implements the WindowsPath subclass of Path that
is aware of Windows drives
- introduces the bazel.windows_unix_root JVM
argument that defines the MSYS root, which
defines the absolute Windows path that is the
root of all Unix paths that Bazel creates (e.g.
"/usr/lib" -> "C:/tools/msys64/usr/lib") except
if the path is of the form "/c/foo" which is
treated as "C:/foo"
- removes all Windows-specific logic from Path
PathFragment is still aware of drive letters and
it has to remain so because it is unaware of file
systems.
WindowsPath restricts the allowed path strings to
absolute Unix paths where the first segment, if
any, is a volume specifier. From now on if Bazel
attempts to create a WindowsPath from an absolute
Unix path, Bazel will make it relative to
WindowsPath.UNIX_ROOT, unless the first component
is a single-letter name (e.g. "/c/foo" which is
"C:/foo").
Subclassing Path is necessary because a Unix-style
absolute path doesn't sufficiently define a full
Windows path, as it may be relative to any drive.
Fixes https://github.com/bazelbuild/bazel/issues/1463
--
MOS_MIGRATED_REVID=136350304
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 e7f1fa2..768792a 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
@@ -22,6 +22,7 @@
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 java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -37,6 +38,20 @@
@ThreadSafe
public abstract class FileSystem {
+ 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);
+ }
+ };
+ }
+
/**
* An exception thrown when attempting to resolve an ordinary file as a symlink.
*/
@@ -49,19 +64,12 @@
protected final Path rootPath;
protected FileSystem() {
- this.rootPath = createRootPath();
+ this.rootPath = getPathFactory().createRootPath(this);
}
- /**
- * Creates the root of all paths used by this filesystem. This is a hook
- * allowing subclasses to define their own root path class. All other paths
- * are created via the root path's {@link Path#createChildPath(String)} method.
- * <p>
- * Beware: this is called during the FileSystem constructor which may occur
- * before subclasses are completely initialized.
- */
- protected Path createRootPath() {
- return new Path(this);
+ /** Returns filesystem-specific path factory. */
+ protected PathFactory getPathFactory() {
+ return UnixPathFactory.INSTANCE;
}
/**
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 dbec227..be95e3b 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
@@ -17,7 +17,6 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.StringCanonicalizer;
import java.io.File;
@@ -31,7 +30,6 @@
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
-import java.util.Arrays;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Objects;
@@ -53,6 +51,28 @@
@ThreadSafe
public class Path implements Comparable<Path>, Serializable {
+ /** 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);
+ }
+
private static FileSystem fileSystemForSerialization;
/**
@@ -151,10 +171,12 @@
private volatile IdentityHashMap<String, Reference<Path>> children;
/**
- * Create a path instance. Should only be called by {@link #createChildPath}.
+ * 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}
+ * StringCanonicalizer#intern}
* @param parent this path's parent
*/
protected Path(FileSystem fileSystem, String name, Path parent) {
@@ -162,6 +184,9 @@
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 {
@@ -170,8 +195,9 @@
}
/**
- * Create the root path. Should only be called by
- * {@link FileSystem#createRootPath()}.
+ * Create the root path.
+ *
+ * <p>Should only be called by {@link PathFactory#createRootPath(FileSystem)}.
*/
protected Path(FileSystem fileSystem) {
this(fileSystem, StringCanonicalizer.intern("/"), null);
@@ -197,6 +223,7 @@
this.depth = this.parent.depth + 1;
}
this.hashCode = Objects.hash(parent, name);
+ reinitializeAfterDeserialization();
}
/**
@@ -211,7 +238,45 @@
}
protected Path createChildPath(String childName) {
- return new Path(fileSystem, childName, this);
+ return fileSystem.getPathFactory().createChildPath(this, childName);
+ }
+
+ /**
+ * Reinitializes this object after deserialization.
+ *
+ * <p>Derived classes should use this hook to initialize additional state.
+ */
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ protected boolean isTopLevelDirectory() {
+ return isRootDirectory();
}
/**
@@ -237,7 +302,7 @@
Reference<Path> childRef = children.get(childName);
Path child;
if (childRef == null || (child = childRef.get()) == null) {
- child = createChildPath(childName);
+ child = fileSystem.getPathFactory().createChildPath(this, childName);
children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE));
}
return child;
@@ -284,37 +349,22 @@
}
/**
- * Computes a string representation of this path, and writes it to the
- * given string builder. Only called locally with a new instance.
+ * Computes a string representation of this path, and writes it to the given string builder. Only
+ * called locally with a new instance.
*/
- private void buildPathString(StringBuilder result) {
+ protected void buildPathString(StringBuilder result) {
if (isRootDirectory()) {
- result.append('/');
+ result.append(PathFragment.ROOT_DIR);
} else {
- if (parent.isWindowsVolumeName()) {
- result.append(parent.name);
- } else {
- parent.buildPathString(result);
- }
+ parent.buildPathString(result);
if (!parent.isRootDirectory()) {
- result.append('/');
+ result.append(PathFragment.SEPARATOR_CHAR);
}
result.append(name);
}
}
/**
- * Returns true if the current path represents a Windows volume name (such as "c:" or "d:").
- *
- * <p>Paths such as '\\\\vol\\foo' are not supported.
- */
- private boolean isWindowsVolumeName() {
- return OS.getCurrent() == OS.WINDOWS
- && parent != null && parent.isRootDirectory() && name.length() == 2
- && PathFragment.getWindowsDriveLetter(name) != '\0';
- }
-
- /**
* Returns the path as a string.
*/
public String getPathString() {
@@ -597,8 +647,8 @@
if (segment.equals(".") || segment.isEmpty()) {
return this; // that's a noop
} else if (segment.equals("..")) {
- // root's parent is root, when canonicalising:
- return parent == null || isWindowsVolumeName() ? this : parent;
+ // top-level directory's parent is root, when canonicalising:
+ return isTopLevelDirectory() ? this : parent;
} else {
return getCachedChildPath(segment);
}
@@ -620,6 +670,10 @@
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.
@@ -630,10 +684,7 @@
* is canonical.
*/
public Path getRelative(PathFragment suffix) {
- Path result = suffix.isAbsolute() ? fileSystem.getRootDirectory() : this;
- if (!suffix.windowsVolume().isEmpty()) {
- result = result.getCanonicalPath(suffix.windowsVolume());
- }
+ Path result = getRootForRelativePathComputation(suffix);
for (String segment : suffix.segments()) {
result = result.getCanonicalPath(segment);
}
@@ -656,7 +707,7 @@
if ((path.length() == 0) || (path.equals("."))) {
return this;
} else if (path.equals("..")) {
- return parent == null ? this : parent;
+ return isTopLevelDirectory() ? this : parent;
} else if (path.indexOf('/') != -1) {
return getRelative(new PathFragment(path));
} else if (path.indexOf(PathFragment.EXTRA_SEPARATOR_CHAR) != -1) {
@@ -666,29 +717,20 @@
}
}
- /**
- * Returns an absolute PathFragment representing this path.
- */
- public PathFragment asFragment() {
+ 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();
}
-
- char driveLetter = '\0';
- if (resultSegments.length > 0) {
- driveLetter = PathFragment.getWindowsDriveLetter(resultSegments[0]);
- if (driveLetter != '\0') {
- // Strip off the first segment that contains the volume name.
- resultSegments = Arrays.copyOfRange(resultSegments, 1, resultSegments.length);
- }
- }
-
- return new PathFragment(driveLetter, true, resultSegments);
+ return resultSegments;
}
+ /** Returns an absolute PathFragment representing this path. */
+ public PathFragment asFragment() {
+ return new PathFragment('\0', true, getSegments());
+ }
/**
* Returns a relative path fragment to this path, relative to {@code
@@ -708,17 +750,19 @@
public PathFragment relativeTo(Path ancestorPath) {
checkSameFilesystem(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 new PathFragment('\0', false, resultSegments);
+ 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 new PathFragment('\0', false, resultSegments);
+ }
}
}
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 9fd130e..2980e29 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
@@ -14,6 +14,7 @@
package com.google.devtools.build.lib.vfs;
import com.google.common.base.Function;
+import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -106,13 +107,14 @@
// live PathFragments, so do not add further fields on a whim.
// The individual path components.
+ // Does *not* include the Windows drive letter.
private final String[] segments;
// True both for UNIX-style absolute paths ("/foo") and Windows-style ("C:/foo").
+ // False for a Windows-style volume label ("C:") which is actually a relative path.
private final boolean isAbsolute;
- // Upper case windows drive letter, or '\0' if none. While a volumeName string is more
- // general, we create a lot of these objects, so space is at a premium.
+ // Upper case Windows drive letter, or '\0' if none or unknown.
private final char driveLetter;
// hashCode and path are lazily initialized but semantically immutable.
@@ -123,13 +125,47 @@
* Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path.
*/
public PathFragment(String path) {
- this.driveLetter = getWindowsDriveLetter(path);
- if (driveLetter != '\0') {
- path = path.substring(2);
- // TODO(bazel-team): Decide what to do about non-absolute paths with a volume name, e.g. C:x.
+ char drive = '\0';
+ boolean abs = false;
+ if (OS.getCurrent() == OS.WINDOWS) {
+ if (path.length() == 2 && isSeparator(path.charAt(0)) && Character.isLetter(path.charAt(1))) {
+ // Path is "/C" or other drive letter
+ drive = Character.toUpperCase(path.charAt(1));
+ abs = true;
+ } else if (path.length() >= 2
+ && Character.isLetter(path.charAt(0))
+ && path.charAt(1) == ':') {
+ // Path is like "C:", "C:/", "C:foo", or "C:/foo"
+ drive = Character.toUpperCase(path.charAt(0));
+ } else if (path.length() >= 3
+ && isSeparator(path.charAt(0))
+ && Character.isLetter(path.charAt(1))) {
+ if (isSeparator(path.charAt(2))) {
+ // Path is like "/C/" or "/C/foo"
+ drive = Character.toUpperCase(path.charAt(1));
+ abs = true;
+ } else if (path.charAt(2) == ':') {
+ // Path is like "/C:" or "/C:/" or "/C:/foo", neither of which is a valid path on MSYS.
+ // They are also very confusing because they would be valid, absolute PathFragments with
+ // no drive letters and the first segment being "C:".
+ // We should not be constructing such PathFragments on Windows because it would allow
+ // creating a valid Path object that would nevertheless be non-equal to "C:/" (because
+ // the internal representation would be different).
+ throw new IllegalArgumentException("Illegal path string \"" + path + "\"");
+ }
+ }
}
- this.isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
+
+ if (drive != '\0') {
+ path = path.substring(2);
+ }
+ this.isAbsolute = abs || (path.length() > 0 && isSeparator(path.charAt(0)));
this.segments = segment(path, isAbsolute ? 1 : 0);
+
+ // If the only difference between this object and EMPTY_FRAGMENT is the drive letter, then this
+ // object is equivalent with the empty fragment. To make them compare equal we must use a null
+ // drive letter.
+ this.driveLetter = (this.isAbsolute || this.segments.length > 0) ? drive : '\0';
}
private static boolean isSeparator(char c) {
@@ -150,6 +186,17 @@
* here in PathFragment, and by Path.asFragment() and Path.relativeTo().
*/
PathFragment(char driveLetter, boolean isAbsolute, String[] segments) {
+ driveLetter = Character.toUpperCase(driveLetter);
+ if (OS.getCurrent() == OS.WINDOWS
+ && segments.length > 0
+ && segments[0].length() == 2
+ && Character.toUpperCase(segments[0].charAt(0)) == driveLetter
+ && segments[0].charAt(1) == ':') {
+ throw new IllegalStateException(
+ String.format(
+ "the drive letter should not be a path segment; drive='%c', segments=[%s]",
+ driveLetter, Joiner.on(", ").join(segments)));
+ }
this.driveLetter = driveLetter;
this.isAbsolute = isAbsolute;
this.segments = segments;
@@ -338,7 +385,11 @@
((driveLetter != '\0') ? 2 : 0)
+ ((segments.length == 0) ? 0 : (segments.length + 1) * 20);
StringBuilder result = new StringBuilder(estimateSize);
- result.append(windowsVolume());
+ 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) {
@@ -412,9 +463,14 @@
if (otherFragment == EMPTY_FRAGMENT) {
return this;
}
- return otherFragment.isAbsolute()
- ? otherFragment
- : new PathFragment(this, otherFragment);
+
+ if (otherFragment.isAbsolute()) {
+ return this.driveLetter == '\0' || otherFragment.driveLetter != '\0'
+ ? otherFragment
+ : new PathFragment(this.driveLetter, true, otherFragment.segments);
+ } else {
+ return new PathFragment(this, otherFragment);
+ }
}
/**
@@ -525,9 +581,9 @@
* order)
*/
public boolean startsWith(PathFragment prefix) {
- if (this.isAbsolute != prefix.isAbsolute ||
- this.segments.length < prefix.segments.length ||
- this.driveLetter != prefix.driveLetter) {
+ if (this.isAbsolute != prefix.isAbsolute
+ || this.segments.length < prefix.segments.length
+ || (isAbsolute && this.driveLetter != prefix.driveLetter)) {
return false;
}
for (int i = 0, len = prefix.segments.length; i < len; i++) {
@@ -613,9 +669,6 @@
}
public String windowsVolume() {
- if (OS.getCurrent() != OS.WINDOWS) {
- return "";
- }
return (driveLetter != '\0') ? driveLetter + ":" : "";
}
@@ -673,16 +726,8 @@
return new PathFragment(driveLetter, false, segments);
}
- /**
- * Given a path, returns the Windows drive letter ('X'), or an null character if no volume
- * name was specified.
- */
- static char getWindowsDriveLetter(String path) {
- if (OS.getCurrent() == OS.WINDOWS
- && path.length() >= 2 && path.charAt(1) == ':' && Character.isLetter(path.charAt(0))) {
- return Character.toUpperCase(path.charAt(0));
- }
- return '\0';
+ private boolean isEmpty() {
+ return !isAbsolute && segments.length == 0;
}
@Override
@@ -694,7 +739,7 @@
// 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 there are the
+ // (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
@@ -705,10 +750,13 @@
// once.
int h = hashCode;
if (h == 0) {
- h = isAbsolute ? 1 : 0;
+ h = Boolean.hashCode(isAbsolute);
for (String segment : segments) {
h = h * 31 + segment.hashCode();
}
+ if (!isEmpty()) {
+ h = h * 31 + Character.hashCode(driveLetter);
+ }
hashCode = h;
}
return h;
@@ -723,8 +771,13 @@
return false;
}
PathFragment otherPath = (PathFragment) other;
- return isAbsolute == otherPath.isAbsolute &&
- Arrays.equals(otherPath.segments, segments);
+ if (isEmpty() && otherPath.isEmpty()) {
+ return true;
+ } else {
+ return isAbsolute == otherPath.isAbsolute
+ && driveLetter == otherPath.driveLetter
+ && Arrays.equals(otherPath.segments, segments);
+ }
}
/**
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 a982595..23e7ad7 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
@@ -14,7 +14,6 @@
package com.google.devtools.build.lib.vfs;
import com.google.devtools.build.lib.util.Preconditions;
-
import java.io.Serializable;
import java.util.Objects;
@@ -49,7 +48,21 @@
* Returns a rooted path representing {@code relativePath} relative to {@code root}.
*/
public static RootedPath toRootedPath(Path root, PathFragment relativePath) {
- return new RootedPath(root, relativePath);
+ if (relativePath.isAbsolute()) {
+ if (root.isRootDirectory()) {
+ return new RootedPath(
+ root.getRelative(relativePath.windowsVolume()), relativePath.toRelative());
+ } else {
+ Preconditions.checkArgument(
+ relativePath.startsWith(root.asFragment()),
+ "relativePath '%s' is absolute, but it's not under root '%s'",
+ relativePath,
+ root);
+ return new RootedPath(root, relativePath.relativeTo(root.asFragment()));
+ }
+ } else {
+ return new RootedPath(root, relativePath);
+ }
}
/**
@@ -57,7 +70,7 @@
*/
public static RootedPath toRootedPath(Path root, Path path) {
Preconditions.checkState(path.startsWith(root), "path: %s root: %s", path, root);
- return new RootedPath(root, path.relativeTo(root));
+ return toRootedPath(root, path.asFragment());
}
/**
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsFileSystem.java
index da04735..bc9a10d 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/WindowsFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/WindowsFileSystem.java
@@ -15,22 +15,205 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.Path.PathFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributes;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
/** Jury-rigged file system for Windows. */
@ThreadSafe
public class WindowsFileSystem extends JavaIoFileSystem {
+ @VisibleForTesting
+ enum WindowsPathFactory implements PathFactory {
+ INSTANCE {
+ @Override
+ public Path createRootPath(FileSystem filesystem) {
+ return new WindowsPath(filesystem, PathFragment.ROOT_DIR, null);
+ }
+
+ @Override
+ public Path createChildPath(Path parent, String childName) {
+ Preconditions.checkState(parent instanceof WindowsPath);
+ return new WindowsPath(parent.getFileSystem(), childName, (WindowsPath) parent);
+ }
+ };
+ }
+
+ private static final class WindowsPath extends Path {
+
+ private static final String WINDOWS_UNIX_ROOT_JVM_ARG = "bazel.windows_unix_root";
+ private static final String BAZEL_SH_ENV_VAR = "BAZEL_SH";
+
+ // Absolute Windows path specifying the root of absolute Unix paths.
+ // This is typically the MSYS installation root, e.g. C:\\tools\\msys64
+ private static final PathFragment UNIX_ROOT =
+ determineUnixRoot(WINDOWS_UNIX_ROOT_JVM_ARG, BAZEL_SH_ENV_VAR);
+
+ // The drive letter is '\0' if and only if this Path is the filesystem root "/".
+ private char driveLetter;
+
+ private WindowsPath(FileSystem fileSystem) {
+ super(fileSystem);
+ this.driveLetter = '\0';
+ }
+
+ private WindowsPath(FileSystem fileSystem, String name, WindowsPath parent) {
+ super(fileSystem, translateName(parent, name), translateParent(parent, name));
+ this.driveLetter = getDriveLetter((WindowsPath) getParentDirectory(), getBaseName());
+ }
+
+ @Override
+ protected void buildPathString(StringBuilder result) {
+ if (isRootDirectory()) {
+ result.append(PathFragment.ROOT_DIR);
+ } else {
+ if (isTopLevelDirectory()) {
+ result.append(driveLetter).append(':').append(PathFragment.SEPARATOR_CHAR);
+ } else {
+ getParentDirectory().buildPathString(result);
+ if (!getParentDirectory().isTopLevelDirectory()) {
+ result.append(PathFragment.SEPARATOR_CHAR);
+ }
+ result.append(getBaseName());
+ }
+ }
+ }
+
+ @Override
+ public void reinitializeAfterDeserialization() {
+ Preconditions.checkState(
+ getParentDirectory().isRootDirectory() || getParentDirectory() instanceof WindowsPath);
+ this.driveLetter =
+ (getParentDirectory() != null) ? ((WindowsPath) getParentDirectory()).driveLetter : '\0';
+ }
+
+ @Override
+ public boolean isMaybeRelativeTo(Path ancestorCandidate) {
+ Preconditions.checkState(ancestorCandidate instanceof WindowsPath);
+ return ancestorCandidate.isRootDirectory()
+ || driveLetter == ((WindowsPath) ancestorCandidate).driveLetter;
+ }
+
+ @Override
+ public boolean isTopLevelDirectory() {
+ return isRootDirectory() || getParentDirectory().isRootDirectory();
+ }
+
+ @Override
+ public PathFragment asFragment() {
+ String[] segments = getSegments();
+ if (segments.length > 0) {
+ // Strip off the first segment that contains the volume name.
+ segments = Arrays.copyOfRange(segments, 1, segments.length);
+ }
+
+ return new PathFragment(driveLetter, true, segments);
+ }
+
+ @Override
+ protected Path getRootForRelativePathComputation(PathFragment relative) {
+ Path result = this;
+ if (relative.isAbsolute()) {
+ result = getFileSystem().getRootDirectory();
+ if (!relative.windowsVolume().isEmpty()) {
+ result = result.getRelative(relative.windowsVolume());
+ }
+ }
+ return result;
+ }
+
+ private static boolean isWindowsVolumeName(String name) {
+ return (name.length() == 1 || (name.length() == 2 && name.charAt(1) == ':'))
+ && Character.isLetter(name.charAt(0));
+ }
+
+ private static String translateName(WindowsPath parent, String name) {
+ if (parent != null && parent.isRootDirectory() && name.length() == 1) {
+ // Path is /c (or similar), which is equivalent to C:/
+ return name.toUpperCase() + ":";
+ } else {
+ return name;
+ }
+ }
+
+ /**
+ * Heuristic to translate absolute Unix paths to Windows paths.
+ *
+ * <p>Unix paths under MSYS should be resolved using /etc/mtab, but reading that every time we
+ * create a Path object would be too expensive.
+ *
+ * <p>As a heuristic, we check if the paths looks like an absolute Unix path (e.g. "/usr/lib")
+ * and make it relative to the MSYS root (yielding "c:/tools/msys64/usr/lib"), but only if the
+ * path doesn't also look like an absolute path on a drive (e.g. /c/windows which means
+ * C:/windows).
+ *
+ * <p>This is an imperfect workaround because the user may have other paths mounted as well, and
+ * this heuristic won't handle those properly, but it's good enough.
+ *
+ * <p>The correct long-term solution is to update all tools on Windows to output Windows paths
+ * and lock down WindowsPath creation to disallow absolute Unix paths.
+ */
+ private static WindowsPath translateParent(WindowsPath parent, String name) {
+ if (parent != null && parent.isRootDirectory() && !isWindowsVolumeName(name)) {
+ // This is a top-level directory which is not a drive name, e.g. "/usr".
+ // Make it relative to UNIX_ROOT.
+ Preconditions.checkNotNull(
+ UNIX_ROOT,
+ "Could not determine Unix path root or it is not an absolute Windows path. Set the "
+ + "\"%s\" JVM argument, or export the \"%s\" environment variable for the MSYS bash"
+ + " and have /usr/bin/cygpath installed",
+ WINDOWS_UNIX_ROOT_JVM_ARG,
+ BAZEL_SH_ENV_VAR);
+
+ return (WindowsPath) parent.getRelative(UNIX_ROOT);
+ } else {
+ // This is not a top-level directory.
+ return parent;
+ }
+ }
+
+ private char getDriveLetter(WindowsPath parent, String name) {
+ if (parent == null) {
+ return '\0';
+ } else {
+ if (parent.isRootDirectory()) {
+ Preconditions.checkState(
+ isWindowsVolumeName(name),
+ "top-level directory on Windows must be a drive (name = '%s')",
+ name);
+ return Character.toUpperCase(name.charAt(0));
+ } else {
+ return parent.driveLetter;
+ }
+ }
+ }
+ }
+
public static final LinkOption[] NO_OPTIONS = new LinkOption[0];
public static final LinkOption[] NO_FOLLOW = new LinkOption[] {LinkOption.NOFOLLOW_LINKS};
@Override
+ protected PathFactory getPathFactory() {
+ return WindowsPathFactory.INSTANCE;
+ }
+
+ @Override
+ public String getFileSystemType(Path path) {
+ // TODO(laszlocsomor): implement this properly, i.e. actually query this information from
+ // somewhere (java.nio.Filesystem? System.getProperty? implement JNI method and use WinAPI?).
+ return "ntfs";
+ }
+
+ @Override
protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
// TODO(lberki): Add some JNI to create hard links/junctions instead of calling out to
// cmd.exe
@@ -213,4 +396,45 @@
}
return false;
}
+
+ private static PathFragment determineUnixRoot(String jvmArgName, String bazelShEnvVar) {
+ // Get the path from a JVM argument, if specified.
+ String path = System.getProperty(jvmArgName);
+
+ if (path == null || path.isEmpty()) {
+ path = "";
+
+ // Fall back to executing cygpath.
+ String bash = System.getenv(bazelShEnvVar);
+ Process process = null;
+ try {
+ process = Runtime.getRuntime().exec(bash + "-c \"/usr/bin/cygpath -m /\"");
+
+ // Wait 3 seconds max, that should be enough to run this command.
+ process.waitFor(3, TimeUnit.SECONDS);
+
+ if (process.exitValue() == 0) {
+ char[] buf = new char[256];
+ try (InputStreamReader r = new InputStreamReader(process.getInputStream())) {
+ int len = 0;
+ while ((len = r.read(buf)) > 0) {
+ path = path + new String(buf, 0, len);
+ }
+ }
+ }
+ } catch (InterruptedException | IOException e) {
+ // Silently ignore failure. Either MSYS is installed at a different location, or not
+ // installed at all, or some error occurred. We can't do anything anymore but throw an
+ // exception if someone tries to create a Path from an absolute Unix path.
+ }
+ }
+
+ path = path.trim();
+ PathFragment result = new PathFragment(path);
+ if (path.isEmpty() || result.getDriveLetter() == '\0' || !result.isAbsolute()) {
+ return null;
+ } else {
+ return result;
+ }
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
index 4830d8f..892fecb 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
@@ -16,7 +16,7 @@
import com.google.common.base.Predicate;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.util.Preconditions;
-
+import com.google.devtools.build.lib.vfs.Path.PathFactory;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
@@ -96,6 +96,23 @@
// #getDirectoryEntries}. Then this field becomes redundant.
@ThreadSafe
private static class ZipPath extends Path {
+
+ private enum Factory implements PathFactory {
+ INSTANCE {
+ @Override
+ public Path createRootPath(FileSystem filesystem) {
+ Preconditions.checkArgument(filesystem instanceof ZipFileSystem);
+ return new ZipPath((ZipFileSystem) filesystem);
+ }
+
+ @Override
+ public Path createChildPath(Path parent, String childName) {
+ Preconditions.checkState(parent instanceof ZipPath);
+ return new ZipPath((ZipFileSystem) parent.getFileSystem(), childName, (ZipPath) parent);
+ }
+ };
+ }
+
/**
* Non-null iff this file/directory exists. Set by setZipEntry for files
* explicitly mentioned in the zipfile's table of contents, or implicitly
@@ -104,12 +121,12 @@
ZipEntry entry = null;
// Root path.
- ZipPath(ZipFileSystem fileSystem) {
+ private ZipPath(ZipFileSystem fileSystem) {
super(fileSystem);
}
// Non-root paths.
- ZipPath(ZipFileSystem fileSystem, String name, ZipPath parent) {
+ private ZipPath(ZipFileSystem fileSystem, String name, ZipPath parent) {
super(fileSystem, name, parent);
}
@@ -128,11 +145,6 @@
path.setZipEntry(new ZipEntry(path + "/")); // trailing "/" => isDir
}
}
-
- @Override
- protected ZipPath createChildPath(String childName) {
- return new ZipPath((ZipFileSystem) getFileSystem(), childName, this);
- }
}
/**
@@ -157,8 +169,8 @@
}
@Override
- protected Path createRootPath() {
- return new ZipPath(this);
+ protected PathFactory getPathFactory() {
+ return ZipPath.Factory.INSTANCE;
}
/** Returns the ZipEntry associated with a given path name, if any. */
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 942d9a3..0556e41 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -169,7 +169,10 @@
java_test(
name = "windows_test",
srcs = CROSS_PLATFORM_WINDOWS_TESTS,
- jvm_flags = ["-Dblaze.os=Windows"],
+ jvm_flags = [
+ "-Dblaze.os=Windows",
+ "-Dbazel.windows_unix_root=C:/fake/msys",
+ ],
test_class = "com.google.devtools.build.lib.AllTests",
deps = [
":foundations_testutil",
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
index ac34ec0..71b7dec 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
@@ -26,14 +26,12 @@
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
import java.io.File;
import java.util.Collections;
import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
* This class tests the functionality of the PathFragment.
@@ -69,15 +67,16 @@
InMemoryFileSystem filesystem = new InMemoryFileSystem();
new EqualsTester()
- .addEqualityGroup(new PathFragment("../relative/path"),
- new PathFragment("../relative/path"),
- new PathFragment(new File("../relative/path")))
+ .addEqualityGroup(
+ new PathFragment("../relative/path"),
+ new PathFragment("..").getRelative("relative").getRelative("path"),
+ new PathFragment('\0', false, new String[] {"..", "relative", "path"}),
+ new PathFragment(new File("../relative/path")))
.addEqualityGroup(new PathFragment("something/else"))
.addEqualityGroup(new PathFragment("/something/else"))
- .addEqualityGroup(new PathFragment("/"),
- new PathFragment("//////"))
- .addEqualityGroup(new PathFragment(""))
- .addEqualityGroup(filesystem.getRootDirectory()) // A Path object.
+ .addEqualityGroup(new PathFragment("/"), new PathFragment("//////"))
+ .addEqualityGroup(new PathFragment(""), PathFragment.EMPTY_FRAGMENT)
+ .addEqualityGroup(filesystem.getRootDirectory()) // A Path object.
.testEquals();
}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
index 48a63e3..df2c770 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
@@ -14,23 +14,24 @@
package com.google.devtools.build.lib.vfs;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import java.io.File;
+import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import java.io.File;
-
/**
* This class tests the functionality of the PathFragment.
*/
@RunWith(JUnit4.class)
public class PathFragmentWindowsTest {
-
+
@Test
public void testWindowsSeparator() {
assertEquals("bar/baz", new PathFragment("bar\\baz").toString());
@@ -52,6 +53,26 @@
}
@Test
+ public void testAbsolutePathsWithDrive() {
+ PathFragment p1 = new PathFragment("/c");
+ assertThat(p1.isAbsolute()).isTrue();
+ assertThat(p1.getDriveLetter()).isEqualTo('C');
+
+ PathFragment p2 = new PathFragment("/c/");
+ assertThat(p2.isAbsolute()).isTrue();
+ assertThat(p2.getDriveLetter()).isEqualTo('C');
+
+ assertThat(p1).isEqualTo(p2);
+
+ try {
+ new PathFragment("/c:");
+ Assert.fail("expected failure");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains("Illegal path string \"/c:\"");
+ }
+ }
+
+ @Test
public void testIsAbsoluteWindowsBackslash() {
assertTrue(new PathFragment(new File("C:\\blah")).isAbsolute());
assertTrue(new PathFragment(new File("C:\\")).isAbsolute());
@@ -83,10 +104,75 @@
assertEquals("C:/c/d", new PathFragment("a/b").getRelative("C:/c/d").getPathString());
}
+ private void assertGetRelative(String path, String relative, PathFragment expected)
+ throws Exception {
+ PathFragment actual = new PathFragment(path).getRelative(relative);
+ assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
+ assertThat(actual).isEqualTo(expected);
+ assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter());
+ assertThat(actual.hashCode()).isEqualTo(expected.hashCode());
+ }
+
+ private void assertRelativeTo(String path, String relativeTo, String... expectedPathSegments)
+ throws Exception {
+ PathFragment expected = new PathFragment('\0', false, expectedPathSegments);
+ PathFragment actual = new PathFragment(path).relativeTo(relativeTo);
+ assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
+ assertThat(actual).isEqualTo(expected);
+ assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter());
+ assertThat(actual.hashCode()).isEqualTo(expected.hashCode());
+ }
+
+ private void assertCantComputeRelativeTo(String path, String relativeTo) throws Exception {
+ try {
+ new PathFragment(path).relativeTo(relativeTo);
+ Assert.fail("expected failure");
+ } catch (Exception e) {
+ assertThat(e.getMessage()).contains("is not beneath");
+ }
+ }
+
+ private static PathFragment makePath(char drive, boolean absolute, String... segments) {
+ return new PathFragment(drive, absolute, segments);
+ }
+
@Test
- public void testGetRelativeMixed() {
- assertEquals("/b", new PathFragment("C:/a").getRelative("/b").getPathString());
- assertEquals("C:/b", new PathFragment("/a").getRelative("C:/b").getPathString());
+ public void testGetRelativeMixed() throws Exception {
+ assertGetRelative("a", "b", makePath('\0', false, "a", "b"));
+ assertGetRelative("a", "/b", makePath('B', true));
+ assertGetRelative("a", "E:b", makePath('\0', false, "a", "b"));
+ assertGetRelative("a", "E:/b", makePath('E', true, "b"));
+
+ assertGetRelative("/a", "b", makePath('A', true, "b"));
+ assertGetRelative("/a", "/b", makePath('B', true));
+ assertGetRelative("/a", "E:b", makePath('A', true, "b"));
+ assertGetRelative("/a", "E:/b", makePath('E', true, "b"));
+
+ assertGetRelative("D:a", "b", makePath('D', false, "a", "b"));
+ assertGetRelative("D:a", "/b", makePath('B', true));
+ assertGetRelative("D:a", "E:b", makePath('D', false, "a", "b"));
+ assertGetRelative("D:a", "E:/b", makePath('E', true, "b"));
+
+ assertGetRelative("D:/a", "b", makePath('D', true, "a", "b"));
+ assertGetRelative("D:/a", "/b", makePath('B', true));
+ assertGetRelative("D:/a", "E:b", makePath('D', true, "a", "b"));
+ assertGetRelative("D:/a", "E:/b", makePath('E', true, "b"));
+ }
+
+ @Test
+ public void testRelativeTo() throws Exception {
+ assertRelativeTo("", "");
+ assertCantComputeRelativeTo("", "a");
+
+ assertRelativeTo("a", "", "a");
+ assertRelativeTo("a", "a");
+ assertCantComputeRelativeTo("a", "b");
+ assertRelativeTo("a/b", "a", "b");
+
+ assertRelativeTo("C:", "");
+ assertRelativeTo("C:", "C:");
+ assertCantComputeRelativeTo("C:/", "");
+ assertRelativeTo("C:/", "C:/");
}
@Test
@@ -97,8 +183,12 @@
// Tests after here test the canonicalization
private void assertRegular(String expected, String actual) {
- assertEquals(expected, new PathFragment(actual).getPathString()); // compare string forms
- assertEquals(new PathFragment(expected), new PathFragment(actual)); // compare fragment forms
+ PathFragment exp = new PathFragment(expected);
+ PathFragment act = new PathFragment(actual);
+ assertThat(exp.getPathString()).isEqualTo(expected);
+ assertThat(act.getPathString()).isEqualTo(expected);
+ assertThat(act).isEqualTo(exp);
+ assertThat(act.hashCode()).isEqualTo(exp.hashCode());
}
@Test
@@ -106,9 +196,38 @@
assertRegular("C:/", "C:/");
}
+ private void assertAllEqual(PathFragment... ps) {
+ assertThat(ps.length).isGreaterThan(1);
+ for (int i = 1; i < ps.length; i++) {
+ String msg = "comparing items 0 and " + i;
+ assertWithMessage(msg + " for getPathString")
+ .that(ps[i].getPathString())
+ .isEqualTo(ps[0].getPathString());
+ assertWithMessage(msg + " for equals").that(ps[0]).isEqualTo(ps[i]);
+ assertWithMessage(msg + " for hashCode").that(ps[0].hashCode()).isEqualTo(ps[i].hashCode());
+ }
+ }
+
@Test
public void testEmptyRelativePathToEmptyPathWindows() {
- assertRegular("C:", "C:");
+ // Surprising but correct behavior: a PathFragment made of just a drive identifier (and not the
+ // absolute path "C:/") is equal not only to the empty fragment, but (therefore) also to other
+ // drive identifiers.
+ // This makes sense if you consider that these are still empty paths, the drive letter adds no
+ // information to the path itself.
+ assertAllEqual(
+ PathFragment.EMPTY_FRAGMENT,
+ new PathFragment("C:"),
+ new PathFragment("D:"),
+ new PathFragment('\0', false, new String[0]),
+ new PathFragment('C', false, new String[0]),
+ new PathFragment('D', false, new String[0]));
+ assertAllEqual(new PathFragment("C:/"), new PathFragment("/c"), new PathFragment("/c/"));
+ assertAllEqual(new PathFragment("C:/foo"), new PathFragment("/c/foo"));
+
+ assertThat(new PathFragment("C:/")).isNotEqualTo(new PathFragment("C:"));
+ assertThat(new PathFragment("C:/").getPathString())
+ .isNotEqualTo(new PathFragment("C:").getPathString());
}
@Test
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java
index cb8b23b..864a483 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathWindowsTest.java
@@ -18,8 +18,10 @@
import static org.junit.Assert.assertSame;
import com.google.devtools.build.lib.util.BlazeClock;
+import com.google.devtools.build.lib.vfs.Path.PathFactory;
+import com.google.devtools.build.lib.vfs.WindowsFileSystem.WindowsPathFactory;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -35,8 +37,16 @@
@Before
public final void initializeFileSystem() throws Exception {
- filesystem = new InMemoryFileSystem(BlazeClock.instance());
- root = filesystem.getRootDirectory();
+ filesystem =
+ new InMemoryFileSystem(BlazeClock.instance()) {
+ @Override
+ protected PathFactory getPathFactory() {
+ return WindowsPathFactory.INSTANCE;
+ }
+ };
+ root = filesystem.getRootDirectory().getRelative("C:/");
+ root.createDirectory();
+
Path first = root.getChild("first");
first.createDirectory();
}
@@ -98,10 +108,38 @@
}
@Test
+ public void testAbsoluteUnixPathIsRelativeToWindowsUnixRoot() {
+ Path actual = root.getRelative("/foo/bar");
+ Path expected = root.getRelative("C:/fake/msys/foo/bar");
+ assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void testAbsoluteUnixPathReferringToDriveIsRecognized() {
+ Path actual = root.getRelative("/c/foo");
+ Path expected = root.getRelative("C:/foo");
+ assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
+ assertThat(actual).isEqualTo(expected);
+
+ // "unexpected" is not a valid MSYS path, we should not be able to create it.
+ try {
+ root.getRelative("/c:");
+ Assert.fail("expected failure");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains("Illegal path string \"/c:\"");
+ }
+ }
+
+ @Test
public void testStartsWithWorksOnWindows() {
assertStartsWithReturnsOnWindows(true, "C:/first/x", "C:/first/x/y");
assertStartsWithReturnsOnWindows(true, "c:/first/x", "C:/FIRST/X/Y");
assertStartsWithReturnsOnWindows(true, "C:/FIRST/X", "c:/first/x/y");
+ assertStartsWithReturnsOnWindows(true, "/", "C:/");
+ assertStartsWithReturnsOnWindows(false, "C:/", "/");
+ assertStartsWithReturnsOnWindows(false, "C:/", "D:/");
+ assertStartsWithReturnsOnWindows(false, "C:/", "D:/foo");
}
@Test
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
index 2be5055..e2317bf 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
@@ -24,12 +24,6 @@
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.testutil.TestUtils;
import com.google.devtools.build.lib.vfs.util.FileSystems;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
@@ -39,6 +33,10 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
* Tests for {@link Path}.
@@ -269,7 +267,7 @@
@Test
public void testDerivedSegmentEquality() {
- Path absoluteSegment = new Path(null);
+ Path absoluteSegment = unixFs.getRootDirectory();
Path derivedNode = absoluteSegment.getChild("derivedSegment");
Path otherDerivedNode = absoluteSegment.getChild("derivedSegment");