PathFragment comparisons are now platform-aware 

PathFragment's `equals`, `hashCode`, `compareTo`,
`startsWith`, `endsWith`, and `relativeTo` are now
aware of case-insensitivity when running on
Windows.

This approach is better than
https://bazel-review.googlesource.com/c/9124/
because it preserves path casing, which is
important when computing action output paths.

This change contains two additional bugfixes:
- `compareTo` now takes `driveLetter` into account
- the `InMemoryFileSystem` in `PathWindowsTest` is
not case-insensitive

Fixes https://github.com/bazelbuild/bazel/issues/2613

--
Change-Id: I1a4250a373fff03fa02a6d8360457450b47a42a8
Reviewed-on: https://cr.bazel.build/9126
PiperOrigin-RevId: 149106930
MOS_MIGRATED_REVID=149106930
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 2bed67d..b466fc9 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
@@ -518,11 +518,9 @@
           + " is not beneath " + ancestorDirectory);
     }
 
-    for (int index = 0; index < ancestorLength; index++) {
-      if (!segments[index].equals(ancestorSegments[index])) {
-        throw new IllegalArgumentException("PathFragment " + this
-            + " is not beneath " + ancestorDirectory);
-      }
+    if (!segmentsEqual(ancestorLength, segments, 0, ancestorSegments)) {
+      throw new IllegalArgumentException(
+          "PathFragment " + this + " is not beneath " + ancestorDirectory);
     }
 
     int length = segments.length - ancestorLength;
@@ -573,12 +571,7 @@
         || (isAbsolute && this.driveLetter != prefix.driveLetter)) {
       return false;
     }
-    for (int i = 0, len = prefix.segments.length; i < len; i++) {
-      if (!this.segments[i].equals(prefix.segments[i])) {
-        return false;
-      }
-    }
-    return true;
+    return segmentsEqual(prefix.segments.length, segments, 0, prefix.segments);
   }
 
   /**
@@ -594,12 +587,7 @@
       return false;
     }
     int offset = this.segments.length - suffix.segments.length;
-    for (int i = 0; i < suffix.segments.length; i++) {
-      if (!this.segments[offset + i].equals(suffix.segments[i])) {
-        return false;
-      }
-    }
-    return true;
+    return segmentsEqual(suffix.segments.length, segments, offset, suffix.segments);
   }
 
   private static String[] subarray(String[] array, int start, int length) {
@@ -736,10 +724,12 @@
     // overhead from synchronization, at the cost of potentially computing the hash code more than
     // once.
     int h = hashCode;
+    boolean isWindows = OS.getCurrent() == OS.WINDOWS;
     if (h == 0) {
       h = Boolean.valueOf(isAbsolute).hashCode();
       for (String segment : segments) {
-        h = h * 31 + segment.hashCode();
+        int segmentHash = isWindows ? segment.toLowerCase().hashCode() : segment.hashCode();
+        h = h * 31 + segmentHash;
       }
       if (!isEmpty()) {
         h = h * 31 + Character.valueOf(driveLetter).hashCode();
@@ -763,10 +753,37 @@
     } else {
       return isAbsolute == otherPath.isAbsolute
           && driveLetter == otherPath.driveLetter
-          && Arrays.equals(otherPath.segments, segments);
+          && segments.length == otherPath.segments.length
+          && segmentsEqual(segments.length, segments, 0, otherPath.segments);
     }
   }
 
+  private static boolean segmentsEqual(
+      int length, String[] segments1, int offset1, String[] segments2) {
+    if ((segments1.length - offset1) < length || segments2.length < length) {
+      return false;
+    }
+    boolean isWindows = OS.getCurrent() == OS.WINDOWS;
+    for (int i = 0; i < length; ++i) {
+      String seg1 = segments1[i + offset1];
+      String seg2 = segments2[i];
+      if ((seg1 == null) != (seg2 == null)) {
+        return false;
+      }
+      if (seg1 == null) {
+        continue;
+      }
+      if (isWindows) {
+        seg1 = seg1.toLowerCase();
+        seg2 = seg2.toLowerCase();
+      }
+      if (!seg1.equals(seg2)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   /**
    * Compares two PathFragments using the lexicographical order.
    */
@@ -775,17 +792,27 @@
     if (isAbsolute != p2.isAbsolute) {
       return isAbsolute ? -1 : 1;
     }
+    int cmp = Character.compare(driveLetter, p2.driveLetter);
+    if (cmp != 0) {
+      return cmp;
+    }
     PathFragment p1 = this;
     String[] segments1 = p1.segments;
     String[] segments2 = p2.segments;
     int len1 = segments1.length;
     int len2 = segments2.length;
     int n = Math.min(len1, len2);
+    boolean isWindows = OS.getCurrent() == OS.WINDOWS;
     for (int i = 0; i < n; i++) {
-      String segment1 = segments1[i];
-      String segment2 = segments2[i];
-      if (!segment1.equals(segment2)) {
-       return segment1.compareTo(segment2);
+      String seg1 = segments1[i];
+      String seg2 = segments2[i];
+      if (isWindows) {
+        seg1 = seg1.toLowerCase();
+        seg2 = seg2.toLowerCase();
+      }
+      cmp = seg1.compareTo(seg2);
+      if (cmp != 0) {
+        return cmp;
       }
     }
     return len1 - len2;
diff --git a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java
index 388c3fd..04015dd 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Path.PathFactory;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
 import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath;
 import java.util.ArrayList;
@@ -65,6 +66,11 @@
           protected PathFactory getPathFactory() {
             return WindowsFileSystem.getPathFactoryForTesting(shortPathResolver);
           }
+
+          @Override
+          public boolean isFilePathCaseSensitive() {
+            return false;
+          }
         };
     root = (WindowsPath) filesystem.getRootDirectory().getRelative("C:/");
     root.createDirectory();
@@ -280,4 +286,53 @@
             "D:/program files/microsoft something/foo/~bar_hello",
             "D:/program files/microsoft something/foo/will.exist");
   }
+
+  @Test
+  public void testCaseInsensitivePathFragment() {
+    // equals
+    assertThat(new PathFragment("c:/FOO/BAR")).isEqualTo(new PathFragment("c:\\foo\\bar"));
+    assertThat(new PathFragment("c:/FOO/BAR")).isNotEqualTo(new PathFragment("d:\\foo\\bar"));
+    assertThat(new PathFragment("c:/FOO/BAR")).isNotEqualTo(new PathFragment("/foo/bar"));
+    // equals for the string representation
+    assertThat(new PathFragment("c:/FOO/BAR").toString())
+        .isNotEqualTo(new PathFragment("c:/foo/bar").toString());
+    // hashCode
+    assertThat(new PathFragment("c:/FOO/BAR").hashCode())
+        .isEqualTo(new PathFragment("c:\\foo\\bar").hashCode());
+    assertThat(new PathFragment("c:/FOO/BAR").hashCode())
+        .isNotEqualTo(new PathFragment("d:\\foo\\bar").hashCode());
+    assertThat(new PathFragment("c:/FOO/BAR").hashCode())
+        .isNotEqualTo(new PathFragment("/foo/bar").hashCode());
+    // compareTo
+    assertThat(new PathFragment("c:/FOO/BAR").compareTo(new PathFragment("c:\\foo\\bar")))
+        .isEqualTo(0);
+    assertThat(new PathFragment("c:/FOO/BAR").compareTo(new PathFragment("d:\\foo\\bar")))
+        .isLessThan(0);
+    assertThat(new PathFragment("c:/FOO/BAR").compareTo(new PathFragment("/foo/bar")))
+        .isGreaterThan(0);
+    // startsWith
+    assertThat(new PathFragment("c:/FOO/BAR").startsWith(new PathFragment("c:\\foo"))).isTrue();
+    assertThat(new PathFragment("c:/FOO/BAR").startsWith(new PathFragment("d:\\foo"))).isFalse();
+    // endsWith
+    assertThat(new PathFragment("c:/FOO/BAR/BAZ").endsWith(new PathFragment("bar\\baz"))).isTrue();
+    assertThat(new PathFragment("c:/FOO/BAR/BAZ").endsWith(new PathFragment("/bar/baz"))).isFalse();
+    assertThat(new PathFragment("c:/FOO/BAR/BAZ").endsWith(new PathFragment("d:\\bar\\baz")))
+        .isFalse();
+    // relativeTo
+    assertThat(new PathFragment("c:/FOO/BAR/BAZ/QUX").relativeTo(new PathFragment("c:\\foo\\bar")))
+        .isEqualTo(new PathFragment("Baz/Qux"));
+  }
+
+  @Test
+  public void testCaseInsensitiveRootedPath() {
+    Path ancestor = filesystem.getPath("C:\\foo\\bar");
+    assertThat(ancestor).isInstanceOf(WindowsPath.class);
+    Path child = filesystem.getPath("C:\\FOO\\Bar\\baz");
+    assertThat(child).isInstanceOf(WindowsPath.class);
+    assertThat(child.startsWith(ancestor)).isTrue();
+    assertThat(child.relativeTo(ancestor)).isEqualTo(new PathFragment("baz"));
+    RootedPath actual = RootedPath.toRootedPath(ancestor, child);
+    assertThat(actual.getRoot()).isEqualTo(ancestor);
+    assertThat(actual.getRelativePath()).isEqualTo(new PathFragment("baz"));
+  }
 }