aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar tomlu <tomlu@google.com>2017-12-12 12:32:22 -0800
committerGravatar Copybara-Service <copybara-piper@google.com>2017-12-12 12:34:25 -0800
commit4a2f2c5c8d582de2914b22d47fc878fcd9da3a04 (patch)
tree82b73bda163ad6f5ecfcf3812fa92aa2afeca88a /src
parent28341132b90e9e315976efcafeec2e510eedf74c (diff)
Add a new file path type, LocalPath.
This path type is a local file path as a wrapper around a string. It works much the same as java.io.File, but without its file operations. For the most part, FilePath shouldn't add much overhead compared to using plain strings. Strings do get normalised on the way in, but no extra objects are allocated unless the path actually needs normalisation. PiperOrigin-RevId: 178798497
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/BUILD3
-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/windows/BUILD22
-rw-r--r--src/main/java/com/google/devtools/build/lib/windows/WindowsShortPath.java44
-rw-r--r--src/test/java/com/google/devtools/build/lib/BUILD4
-rw-r--r--src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java180
-rw-r--r--src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java48
-rw-r--r--src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java169
-rw-r--r--src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java164
-rw-r--r--src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java43
-rw-r--r--src/test/java/com/google/devtools/build/lib/windows/WindowsShortPathTest.java66
11 files changed, 1411 insertions, 47 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 95b9ca8e6c..7beb483fa3 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
@@ -45,12 +45,15 @@ java_library(
deps = [
":pathfragment",
"//src/main/java/com/google/devtools/build/lib:base-util",
+ "//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",
"//src/main/java/com/google/devtools/build/lib/profiler",
"//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/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/LocalPath.java b/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
new file mode 100644
index 0000000000..a32a4e356c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
@@ -0,0 +1,715 @@
+// 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/windows/BUILD b/src/main/java/com/google/devtools/build/lib/windows/BUILD
index 2b85ad8ff6..4b6e08c73a 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/windows/BUILD
@@ -7,12 +7,28 @@ filegroup(
)
java_library(
+ name = "windows_short_path",
+ srcs = ["WindowsShortPath.java"],
+ visibility = [
+ "//src/main/java/com/google/devtools/build/lib/vfs:__pkg__",
+ "//src/test/java/com/google/devtools/build/lib:__pkg__",
+ ],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib/windows/jni",
+ "//third_party:jsr305",
+ ],
+)
+
+java_library(
name = "windows",
- srcs = glob([
- "*.java",
- ]),
+ srcs = [
+ "WindowsFileSystem.java",
+ "WindowsSubprocess.java",
+ "WindowsSubprocessFactory.java",
+ ],
visibility = ["//visibility:public"],
deps = [
+ ":windows_short_path",
"//src/main/java/com/google/devtools/build/lib:base-util",
"//src/main/java/com/google/devtools/build/lib:os_util",
"//src/main/java/com/google/devtools/build/lib/clock",
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsShortPath.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsShortPath.java
new file mode 100644
index 0000000000..d1527f0f1e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsShortPath.java
@@ -0,0 +1,44 @@
+// 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.windows;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Support functions for Windows short paths (eg. "C:/progra~1") */
+public final class WindowsShortPath {
+
+ // Properties of 8dot3 (DOS-style) short file names:
+ // - they are at most 11 characters long
+ // - they have a prefix (before "~") that is {1..6} characters long, may contain numbers, letters,
+ // "_", even "~", and maybe even more
+ // - they have a "~" after the prefix
+ // - have {1..6} numbers after "~" (according to [1] this is only one digit, but MSDN doesn't
+ // clarify this), the combined length up till this point is at most 8
+ // - they have an optional "." afterwards, and another {0..3} more characters
+ // - just because a path looks like a short name it isn't necessarily one; the user may create
+ // such names and they'd resolve to themselves
+ // [1] https://en.wikipedia.org/wiki/8.3_filename#VFAT_and_Computer-generated_8.3_filenames
+ // bullet point (3) (on 2016-12-05)
+ private static final Pattern PATTERN = Pattern.compile("^(.{1,6})~([0-9]{1,6})(\\..{0,3}){0,1}");
+
+ /** Matches a single path segment for whether it could be a Windows short path. */
+ public static boolean isShortPath(String segment) {
+ Matcher m = PATTERN.matcher(segment);
+ return segment.length() <= 12
+ && m.matches()
+ && m.groupCount() >= 2
+ && (m.group(1).length() + m.group(2).length()) < 8; // the "~" makes it at most 8
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index ba38e5698e..2b3280ab53 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -9,6 +9,7 @@ package(
CROSS_PLATFORM_WINDOWS_TESTS = [
"util/DependencySetWindowsTest.java",
"vfs/PathFragmentWindowsTest.java",
+ "vfs/WindowsLocalPathTest.java",
"windows/PathWindowsTest.java",
]
@@ -207,7 +208,7 @@ java_test(
# systems
java_test(
name = "windows_test",
- srcs = CROSS_PLATFORM_WINDOWS_TESTS,
+ srcs = CROSS_PLATFORM_WINDOWS_TESTS + ["vfs/LocalPathAbstractTest.java"],
jvm_flags = [
"-Dblaze.os=Windows",
"-Dbazel.windows_unix_root=C:/fake/msys",
@@ -268,6 +269,7 @@ java_test(
"//src/main/java/com/google/devtools/build/lib/vfs",
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
"//src/main/java/com/google/devtools/build/lib/windows",
+ "//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:junit4",
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java b/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java
new file mode 100644
index 0000000000..93869521ad
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java
@@ -0,0 +1,180 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link LocalPath}. */
+public abstract class LocalPathAbstractTest {
+
+ private OsPathPolicy os;
+
+ @Before
+ public void setup() {
+ os = getFilePathOs();
+ }
+
+ @Test
+ public void testEqualsAndHashCode() {
+ new EqualsTester()
+ .addEqualityGroup(
+ create("../relative/path"), create("..").getRelative("relative").getRelative("path"))
+ .addEqualityGroup(create("something/else"))
+ .addEqualityGroup(create(""), LocalPath.EMPTY)
+ .testEquals();
+ }
+
+ @Test
+ public void testRelativeTo() {
+ assertThat(create("").relativeTo(create("")).getPathString()).isEmpty();
+ assertThat(create("foo").relativeTo(create("foo")).getPathString()).isEmpty();
+ assertThat(create("foo/bar/baz").relativeTo(create("foo")).getPathString())
+ .isEqualTo("bar/baz");
+ assertThat(create("foo/bar/baz").relativeTo(create("foo/bar")).getPathString())
+ .isEqualTo("baz");
+ assertThat(create("foo").relativeTo(create("")).getPathString()).isEqualTo("foo");
+
+ // Cannot relativize non-ancestors
+ assertThrows(IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("fo")));
+
+ // Make sure partial directory matches aren't reported
+ assertThrows(
+ IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("foo/ba")));
+ }
+
+ @Test
+ public void testGetRelative() {
+ assertThat(create("a").getRelative("b").getPathString()).isEqualTo("a/b");
+ assertThat(create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d");
+ assertThat(create("a").getRelative("").getPathString()).isEqualTo("a");
+ assertThat(create("a/b").getRelative("../c").getPathString()).isEqualTo("a/c");
+ assertThat(create("a/b").getRelative("..").getPathString()).isEqualTo("a");
+ }
+
+ @Test
+ public void testEmptyPathToEmptyPath() {
+ // compare string forms
+ assertThat(create("").getPathString()).isEmpty();
+ // compare fragment forms
+ assertThat(create("")).isEqualTo(create(""));
+ }
+
+ @Test
+ public void testSimpleNameToSimpleName() {
+ // compare string forms
+ assertThat(create("foo").getPathString()).isEqualTo("foo");
+ // compare fragment forms
+ assertThat(create("foo")).isEqualTo(create("foo"));
+ }
+
+ @Test
+ public void testSimplePathToSimplePath() {
+ // compare string forms
+ assertThat(create("foo/bar").getPathString()).isEqualTo("foo/bar");
+ // compare fragment forms
+ assertThat(create("foo/bar")).isEqualTo(create("foo/bar"));
+ }
+
+ @Test
+ public void testStripsTrailingSlash() {
+ // compare string forms
+ assertThat(create("foo/bar/").getPathString()).isEqualTo("foo/bar");
+ // compare fragment forms
+ assertThat(create("foo/bar/")).isEqualTo(create("foo/bar"));
+ }
+
+ @Test
+ public void testGetParentDirectory() {
+ LocalPath fooBarWiz = create("foo/bar/wiz");
+ LocalPath fooBar = create("foo/bar");
+ LocalPath foo = create("foo");
+ LocalPath empty = create("");
+ assertThat(fooBarWiz.getParentDirectory()).isEqualTo(fooBar);
+ assertThat(fooBar.getParentDirectory()).isEqualTo(foo);
+ assertThat(foo.getParentDirectory()).isEqualTo(empty);
+ assertThat(empty.getParentDirectory()).isNull();
+ }
+
+ @Test
+ public void testBasename() throws Exception {
+ assertThat(create("foo/bar").getBaseName()).isEqualTo("bar");
+ assertThat(create("foo/").getBaseName()).isEqualTo("foo");
+ assertThat(create("foo").getBaseName()).isEqualTo("foo");
+ assertThat(create("").getBaseName()).isEmpty();
+ }
+
+ @Test
+ public void testStartsWith() {
+ // (relative path, relative prefix) => true
+ assertThat(create("foo/bar").startsWith(create("foo/bar"))).isTrue();
+ assertThat(create("foo/bar").startsWith(create("foo"))).isTrue();
+ assertThat(create("foot/bar").startsWith(create("foo"))).isFalse();
+ }
+
+ @Test
+ public void testNormalize() {
+ assertThat(create("a/b")).isEqualTo(create("a/b"));
+ assertThat(create("a/../../b")).isEqualTo(create("../b"));
+ assertThat(create("a/../..")).isEqualTo(create(".."));
+ assertThat(create("a/../b")).isEqualTo(create("b"));
+ assertThat(create("a/b/../b")).isEqualTo(create("a/b"));
+ }
+
+ @Test
+ public void testNormalStringsDoNotAllocate() {
+ String normal1 = "a/b/hello.txt";
+ assertThat(create(normal1).getPathString()).isSameAs(normal1);
+
+ // Sanity check our testing strategy
+ String notNormal = "a/../b";
+ assertThat(create(notNormal).getPathString()).isNotSameAs(notNormal);
+ }
+
+ @Test
+ public void testComparableSortOrder() {
+ List<LocalPath> list =
+ Lists.newArrayList(
+ create("zzz"),
+ create("ZZZ"),
+ create("ABC"),
+ create("aBc"),
+ create("AbC"),
+ create("abc"));
+ Collections.sort(list);
+ List<String> result = list.stream().map(LocalPath::getPathString).collect(toList());
+
+ if (os.isCaseSensitive()) {
+ assertThat(result).containsExactly("ABC", "AbC", "ZZZ", "aBc", "abc", "zzz").inOrder();
+ } else {
+ // Partial ordering among case-insensitive items guaranteed by Collections.sort stability
+ assertThat(result).containsExactly("ABC", "aBc", "AbC", "abc", "zzz", "ZZZ").inOrder();
+ }
+ }
+
+ protected abstract OsPathPolicy getFilePathOs();
+
+ protected LocalPath create(String path) {
+ return LocalPath.createWithOs(path, os);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java
new file mode 100644
index 0000000000..c99ad487ee
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java
@@ -0,0 +1,48 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.vfs.LocalPath.MacOsPathPolicy;
+import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests Mac-specific parts of {@link LocalPath}. */
+@RunWith(JUnit4.class)
+public class MacOsLocalPathTest extends UnixLocalPathTest {
+
+ @Override
+ protected OsPathPolicy getFilePathOs() {
+ return new MacOsPathPolicy();
+ }
+
+ @Test
+ public void testMacEqualsAndHashCode() {
+ new EqualsTester()
+ .addEqualityGroup(create("a/b"), create("A/B"))
+ .addEqualityGroup(create("/a/b"), create("/A/B"))
+ .addEqualityGroup(create("something/else"))
+ .addEqualityGroup(create("/something/else"))
+ .testEquals();
+ }
+
+ @Test
+ public void testCaseIsPreserved() {
+ assertThat(create("a/B").getPathString()).isEqualTo("a/B");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java
new file mode 100644
index 0000000000..2cdb4014ae
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java
@@ -0,0 +1,169 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
+import com.google.devtools.build.lib.vfs.LocalPath.UnixOsPathPolicy;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests the unix implementation of {@link LocalPath}. */
+@RunWith(JUnit4.class)
+public class UnixLocalPathTest extends LocalPathAbstractTest {
+
+ @Test
+ public void testEqualsAndHashCodeUnix() {
+ new EqualsTester()
+ .addEqualityGroup(create("/something/else"))
+ .addEqualityGroup(create("/"), create("//////"))
+ .testEquals();
+ }
+
+ @Test
+ public void testRelativeToUnix() {
+ // Cannot relativize absolute and non-absolute
+ assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
+ assertThat(create("/").relativeTo(create("/")).getPathString()).isEmpty();
+ assertThat(create("/foo").relativeTo(create("/foo")).getPathString()).isEmpty();
+ assertThat(create("/foo/bar/baz").relativeTo(create("/foo")).getPathString())
+ .isEqualTo("bar/baz");
+ assertThat(create("/foo/bar/baz").relativeTo(create("/foo/bar")).getPathString())
+ .isEqualTo("baz");
+ assertThat(create("/foo").relativeTo(create("/")).getPathString()).isEqualTo("foo");
+ assertThrows(
+ IllegalArgumentException.class, () -> create("/foo/bar/baz").relativeTo(create("foo")));
+ assertThrows(
+ IllegalArgumentException.class, () -> create("/foo").relativeTo(create("/foo/bar/baz")));
+ }
+
+ @Test
+ public void testIsAbsoluteUnix() {
+ assertThat(create("/absolute/test").isAbsolute()).isTrue();
+ assertThat(create("relative/test").isAbsolute()).isFalse();
+ }
+
+ @Test
+ public void testGetRelativeUnix() {
+ assertThat(create("/a").getRelative("b").getPathString()).isEqualTo("/a/b");
+ assertThat(create("/").getRelative("").getPathString()).isEqualTo("/");
+ assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
+ }
+
+ @Test
+ public void testEmptyPathToEmptyPathUnix() {
+ // compare string forms
+ assertThat(create("/").getPathString()).isEqualTo("/");
+ // compare fragment forms
+ assertThat(create("/")).isEqualTo(create("/"));
+ }
+
+ @Test
+ public void testRedundantSlashes() {
+ // compare string forms
+ assertThat(create("///").getPathString()).isEqualTo("/");
+ // compare fragment forms
+ assertThat(create("///")).isEqualTo(create("/"));
+ // compare string forms
+ assertThat(create("/foo///bar").getPathString()).isEqualTo("/foo/bar");
+ // compare fragment forms
+ assertThat(create("/foo///bar")).isEqualTo(create("/foo/bar"));
+ // compare string forms
+ assertThat(create("////foo//bar").getPathString()).isEqualTo("/foo/bar");
+ // compare fragment forms
+ assertThat(create("////foo//bar")).isEqualTo(create("/foo/bar"));
+ }
+
+ @Test
+ public void testSimpleNameToSimpleNameUnix() {
+ // compare string forms
+ assertThat(create("/foo").getPathString()).isEqualTo("/foo");
+ // compare fragment forms
+ assertThat(create("/foo")).isEqualTo(create("/foo"));
+ }
+
+ @Test
+ public void testSimplePathToSimplePathUnix() {
+ // compare string forms
+ assertThat(create("/foo/bar").getPathString()).isEqualTo("/foo/bar");
+ // compare fragment forms
+ assertThat(create("/foo/bar")).isEqualTo(create("/foo/bar"));
+ }
+
+ @Test
+ public void testGetParentDirectoryUnix() {
+ LocalPath fooBarWizAbs = create("/foo/bar/wiz");
+ LocalPath fooBarAbs = create("/foo/bar");
+ LocalPath fooAbs = create("/foo");
+ LocalPath rootAbs = create("/");
+ assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs);
+ assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs);
+ assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs);
+ assertThat(rootAbs.getParentDirectory()).isNull();
+ }
+
+ @Test
+ public void testBasenameUnix() throws Exception {
+ assertThat(create("/foo/bar").getBaseName()).isEqualTo("bar");
+ assertThat(create("/foo/").getBaseName()).isEqualTo("foo");
+ assertThat(create("/foo").getBaseName()).isEqualTo("foo");
+ assertThat(create("/").getBaseName()).isEmpty();
+ }
+
+ @Test
+ public void testStartsWithUnix() {
+ LocalPath foobar = create("/foo/bar");
+ LocalPath foobarRelative = create("foo/bar");
+
+ // (path, prefix) => true
+ assertThat(foobar.startsWith(foobar)).isTrue();
+ assertThat(foobar.startsWith(create("/"))).isTrue();
+ assertThat(foobar.startsWith(create("/foo"))).isTrue();
+ assertThat(foobar.startsWith(create("/foo/"))).isTrue();
+ assertThat(foobar.startsWith(create("/foo/bar/"))).isTrue(); // Includes trailing slash.
+
+ // (prefix, path) => false
+ assertThat(create("/foo").startsWith(foobar)).isFalse();
+ assertThat(create("/").startsWith(foobar)).isFalse();
+
+ // (absolute, relative) => false
+ assertThat(foobar.startsWith(foobarRelative)).isFalse();
+ assertThat(foobarRelative.startsWith(foobar)).isFalse();
+
+ // relative paths start with nothing, absolute paths do not
+ assertThat(foobar.startsWith(create(""))).isFalse();
+
+ // (path, sibling) => false
+ assertThat(create("/foo/wiz").startsWith(foobar)).isFalse();
+ assertThat(foobar.startsWith(create("/foo/wiz"))).isFalse();
+ }
+
+ @Test
+ public void testNormalizeUnix() {
+ assertThat(create("/a/b")).isEqualTo(create("/a/b"));
+ assertThat(create("/a/b/")).isEqualTo(create("/a/b"));
+ assertThat(create("/a/./b")).isEqualTo(create("/a/b"));
+ assertThat(create("/a/../b")).isEqualTo(create("/b"));
+ assertThat(create("/..")).isEqualTo(create("/.."));
+ }
+
+ @Override
+ protected OsPathPolicy getFilePathOs() {
+ return new UnixOsPathPolicy();
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java
new file mode 100644
index 0000000000..ac5acef1f4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java
@@ -0,0 +1,164 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
+import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
+import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy;
+import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy.ShortPathResolver;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests windows-specific parts of {@link LocalPath} */
+@RunWith(JUnit4.class)
+public class WindowsLocalPathTest extends LocalPathAbstractTest {
+
+ private static final class MockShortPathResolver implements ShortPathResolver {
+ // Full path to resolved child mapping.
+ private Map<String, String> resolutions = new HashMap<>();
+
+ @Override
+ public String resolveShortPath(String path) {
+ String[] segments = path.split("[\\\\/]+");
+ String result = "";
+ for (int i = 0; i < segments.length; ) {
+ String segment = segments[i];
+ String queryString = (result + segment).toLowerCase();
+ segment = resolutions.getOrDefault(queryString, segment);
+ result = result + segment;
+ ++i;
+ if (i != segments.length) {
+ result += "/";
+ }
+ }
+ return result;
+ }
+ }
+
+ private final MockShortPathResolver shortPathResolver = new MockShortPathResolver();
+
+ @Override
+ protected OsPathPolicy getFilePathOs() {
+ return new WindowsOsPathPolicy(shortPathResolver);
+ }
+
+ @Test
+ public void testEqualsAndHashcodeWindows() {
+ new EqualsTester()
+ .addEqualityGroup(create("a/b"), create("A/B"))
+ .addEqualityGroup(create("/a/b"), create("/A/B"))
+ .addEqualityGroup(create("c:/a/b"), create("C:\\A\\B"))
+ .addEqualityGroup(create("something/else"))
+ .testEquals();
+ }
+
+ @Test
+ public void testCaseIsPreserved() {
+ assertThat(create("a/B").getPathString()).isEqualTo("a/B");
+ }
+
+ @Test
+ public void testNormalizeWindows() {
+ assertThat(create("C:/")).isEqualTo(create("C:/"));
+ assertThat(create("c:/")).isEqualTo(create("C:/"));
+ assertThat(create("c:\\")).isEqualTo(create("C:/"));
+ assertThat(create("c:\\foo\\..\\bar\\")).isEqualTo(create("C:/bar"));
+ }
+
+ @Test
+ public void testStartsWithWindows() {
+ assertThat(create("C:/").startsWith(create("C:/"))).isTrue();
+ assertThat(create("C:/foo").startsWith(create("C:/"))).isTrue();
+ assertThat(create("C:/foo").startsWith(create("D:/"))).isFalse();
+
+ // Case insensitivity test
+ assertThat(create("C:/foo/bar").startsWith(create("C:/FOO"))).isTrue();
+ }
+
+ @Test
+ public void testGetParentDirectoryWindows() {
+ assertThat(create("C:/foo").getParentDirectory()).isEqualTo(create("C:/"));
+ assertThat(create("C:/").getParentDirectory()).isNull();
+ }
+
+ @Test
+ public void testisAbsoluteWindows() {
+ assertThat(create("C:/").isAbsolute()).isTrue();
+ // test that msys paths turn into absolute paths
+ assertThat(create("/").isAbsolute()).isTrue();
+ }
+
+ @Test
+ public void testRelativeToWindows() {
+ assertThat(create("C:/foo").relativeTo(create("C:/"))).isEqualTo(create("foo"));
+ // Case insensitivity test
+ assertThat(create("C:/foo/bar").relativeTo(create("C:/FOO"))).isEqualTo(create("bar"));
+ MoreAsserts.assertThrows(
+ IllegalArgumentException.class, () -> create("D:/foo").relativeTo(create("C:/")));
+ }
+
+ @Test
+ public void testAbsoluteUnixPathIsRelativeToWindowsUnixRoot() {
+ assertThat(create("/").getPathString()).isEqualTo("C:/fake/msys");
+ assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar");
+ assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar");
+ }
+
+ @Test
+ public void testAbsoluteUnixPathReferringToDriveIsRecognized() {
+ assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo");
+ assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo");
+ assertThat(create("/c:").getPathString()).isNotEqualTo("C:/foo");
+ }
+
+ @Test
+ public void testResolvesShortenedPaths() {
+ shortPathResolver.resolutions.put("d:/progra~1", "program files");
+ shortPathResolver.resolutions.put("d:/program files/micros~1", "microsoft something");
+ shortPathResolver.resolutions.put(
+ "d:/program files/microsoft something/foo/~bar~1", "~bar_hello");
+
+ // Assert normal shortpath resolution.
+ LocalPath normal = create("d:/progra~1/micros~1/foo/~bar~1/baz");
+ // The path string has an upper-case drive letter because that's how path printing works.
+ assertThat(normal.getPathString())
+ .isEqualTo("D:/program files/microsoft something/foo/~bar_hello/baz");
+ LocalPath notYetExistent = create("d:/progra~1/micros~1/foo/will~1.exi/bar");
+ // The path string has an upper-case drive letter because that's how path printing works.
+ assertThat(notYetExistent.getPathString())
+ .isEqualTo("D:/program files/microsoft something/foo/will~1.exi/bar");
+
+ LocalPath msRoot = create("d:/progra~1/micros~1");
+ assertThat(msRoot.getPathString()).isEqualTo("D:/program files/microsoft something");
+
+ // Pretend that a path we already failed to resolve once came into existence.
+ shortPathResolver.resolutions.put(
+ "d:/program files/microsoft something/foo/will~1.exi", "will.exist");
+
+ // Assert that this time we can resolve the previously non-existent path.
+ LocalPath nowExists = create("d:/progra~1/micros~1/foo/will~1.exi/bar");
+ // The path string has an upper-case drive letter because that's how path printing works.
+ assertThat(nowExists.getPathString())
+ .isEqualTo("D:/program files/microsoft something/foo/will.exist/bar");
+
+ // Assert relative paths that look like short paths are untouched
+ assertThat(create("progra~1").getPathString()).isEqualTo("progra~1");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
index e52c9b65bc..8937c453a7 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
@@ -15,7 +15,6 @@
package com.google.devtools.build.lib.windows;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.lib.windows.WindowsFileSystem.SHORT_NAME_MATCHER;
import static org.junit.Assert.fail;
import com.google.common.base.Function;
@@ -68,48 +67,6 @@ public class WindowsFileSystemTest {
}
@Test
- public void testShortNameMatcher() {
- assertThat(SHORT_NAME_MATCHER.apply("abc")).isFalse(); // no ~ in the name
- assertThat(SHORT_NAME_MATCHER.apply("abc~")).isFalse(); // no number after the ~
- assertThat(SHORT_NAME_MATCHER.apply("~abc")).isFalse(); // no ~ followed by number
- assertThat(SHORT_NAME_MATCHER.apply("too_long_path")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("too_long_path~1")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("abcd~1234")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("h~1")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("h~12")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("h~12.")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("h~12.a")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("h~12.abc")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("h~123456")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hellow~1")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hellow~1.")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hellow~1.a")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hellow~1.abc")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hello~1.abcd")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hellow~1.abcd")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hello~12")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hello~12.")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hello~12.a")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hello~12.abc")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("hello~12.abcd")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hellow~12")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hellow~12.")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hellow~12.a")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("hellow~12.ab")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("~h~1")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~1.")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~1.a")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~1.abc")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~1.abcd")).isFalse(); // too long for 8dot3
- assertThat(SHORT_NAME_MATCHER.apply("~h~12")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~12~1")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.a")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.abc")).isTrue();
- assertThat(SHORT_NAME_MATCHER.apply("~h~12~1.abcd")).isFalse(); // too long for 8dot3
- }
-
- @Test
public void testCanWorkWithJunctionSymlinks() throws Exception {
testUtil.scratchFile("dir\\hello.txt", "hello");
testUtil.scratchDir("non_existent");
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsShortPathTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsShortPathTest.java
new file mode 100644
index 0000000000..2a57612980
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsShortPathTest.java
@@ -0,0 +1,66 @@
+// 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.windows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link WindowsShortPath}. */
+@RunWith(JUnit4.class)
+public class WindowsShortPathTest {
+ @Test
+ public void testShortNameMatcher() {
+ assertThat(WindowsShortPath.isShortPath("abc")).isFalse(); // no ~ in the name
+ assertThat(WindowsShortPath.isShortPath("abc~")).isFalse(); // no number after the ~
+ assertThat(WindowsShortPath.isShortPath("~abc")).isFalse(); // no ~ followed by number
+ assertThat(WindowsShortPath.isShortPath("too_long_path")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("too_long_path~1")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("abcd~1234")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("h~1")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("h~12")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("h~12.")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("h~12.a")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("h~12.abc")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("h~123456")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hellow~1")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hellow~1.")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hellow~1.a")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hellow~1.abc")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hello~1.abcd")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hellow~1.abcd")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hello~12")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hello~12.")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hello~12.a")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hello~12.abc")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("hello~12.abcd")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hellow~12")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hellow~12.")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hellow~12.a")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("hellow~12.ab")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("~h~1")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~1.")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~1.a")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~1.abc")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~1.abcd")).isFalse(); // too long for 8dot3
+ assertThat(WindowsShortPath.isShortPath("~h~12")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~12~1")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~12~1.")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~12~1.a")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~12~1.abc")).isTrue();
+ assertThat(WindowsShortPath.isShortPath("~h~12~1.abcd")).isFalse(); // too long for 8dot3
+ }
+}