VFS: implement a Windows-specific Path subclass

This change rolls forward commit e0d7a540e3c615c628f63fcaaaba0c47fca2cb25 and
commit 8bb4299b28de14eed9d3b57bcaeb9350c81c7db3, and adds a bugfix:
- FileSystem.PathFactory got a new translatePath
  method that WindowsFileSystem.PathFactory
  overrides to translate absolute Unix paths to
  MSYS-relative paths
- Path.getCachedChildPath calls this translatePath
  method so the child path is registered with the
  correct (translated) parent and under the
  correct name (e.g. "C:" instead of say "c")

Below is the rest of the original change
description:

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=137149483
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..0d30e1f 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
@@ -17,9 +17,14 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 
+import com.google.common.base.Predicate;
 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 java.util.ArrayList;
+import java.util.List;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,8 +40,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 +111,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
@@ -119,4 +160,38 @@
     Path child = windowsFileSystem.getPath(descendant);
     assertEquals(expected, child.startsWith(parent));
   }
+
+  @Test
+  public void testChildRegistrationWithTranslatedPaths() {
+    // Ensure the Path to "/usr" (actually "C:/fake/msys/usr") is created, path parents/children
+    // properly registered.
+    Path usrPath = root.getRelative("/usr");
+
+    // Assert that "usr" is not registered as a child of "/".
+    final List<String> children = new ArrayList<>(2);
+    root.applyToChildren(
+        new Predicate<Path>() {
+          @Override
+          public boolean apply(Path input) {
+            children.add(input.getPathString());
+            return true;
+          }
+        });
+    assertThat(children).containsExactly("C:/fake", "C:/first");
+
+    // Assert that "usr" is registered as a child of "C:/fake/msys/".
+    children.clear();
+    root.getRelative("C:/fake/msys")
+        .applyToChildren(
+            new Predicate<Path>() {
+              @Override
+              public boolean apply(Path input) {
+                children.add(input.getPathString());
+                return true;
+              }
+            });
+    assertThat(children).containsExactly("C:/fake/msys/usr");
+
+    assertThat(usrPath).isEqualTo(root.getRelative("C:/fake/msys/usr"));
+  }
 }