Make FileSystem operate on LocalPath instead of Path.

PiperOrigin-RevId: 179082062
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
index dc52388..aaac32e 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BinTools.java
@@ -194,7 +194,7 @@
   }
 
   private void linkTool(Path sourcePath, Path linkPath) throws ExecException {
-    if (linkPath.getFileSystem().supportsSymbolicLinksNatively(linkPath)) {
+    if (linkPath.getFileSystem().supportsSymbolicLinksNatively(linkPath.getLocalPath())) {
       try {
         if (!linkPath.isSymbolicLink()) {
           // ensureSymbolicLink() does not handle the case where there is already
diff --git a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
index 7b82dcc..4a79ee1 100644
--- a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
@@ -16,6 +16,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Striped;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.profiler.Profiler;
 import com.google.devtools.build.lib.profiler.ProfilerTask;
@@ -24,18 +25,19 @@
 import com.google.devtools.build.lib.vfs.AbstractFileSystemWithCustomStat;
 import com.google.devtools.build.lib.vfs.Dirent;
 import com.google.devtools.build.lib.vfs.FileStatus;
-import com.google.devtools.build.lib.vfs.Path;
-import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.LocalPath;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.locks.Lock;
 
 /**
  * This class implements the FileSystem interface using direct calls to the UNIX filesystem.
  */
 @ThreadSafe
 public class UnixFileSystem extends AbstractFileSystemWithCustomStat {
+  private final Striped<Lock> pathLock = Striped.lock(64);
 
   public UnixFileSystem() {
   }
@@ -100,7 +102,7 @@
   }
 
   @Override
-  protected Collection<String> getDirectoryEntries(Path path) throws IOException {
+  protected Collection<String> getDirectoryEntries(LocalPath path) throws IOException {
     String name = path.getPathString();
     String[] entries;
     long startTime = Profiler.nanoTimeMaybe();
@@ -117,7 +119,7 @@
   }
 
   @Override
-  protected PathFragment resolveOneLink(Path path) throws IOException {
+  protected String resolveOneLink(LocalPath path) throws IOException {
     // Beware, this seemingly simple code belies the complex specification of
     // FileSystem.resolveOneLink().
     return stat(path, false).isSymbolicLink()
@@ -145,7 +147,7 @@
   }
 
   @Override
-  protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+  protected Collection<Dirent> readdir(LocalPath path, boolean followSymlinks) throws IOException {
     String name = path.getPathString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -164,12 +166,12 @@
   }
 
   @Override
-  protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException {
     return statInternal(path, followSymlinks);
   }
 
   @VisibleForTesting
-  protected UnixFileStatus statInternal(Path path, boolean followSymlinks) throws IOException {
+  protected UnixFileStatus statInternal(LocalPath path, boolean followSymlinks) throws IOException {
     String name = path.getPathString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -185,7 +187,7 @@
   // This is a performance optimization in the case where clients
   // catch and don't re-throw.
   @Override
-  protected FileStatus statNullable(Path path, boolean followSymlinks) {
+  protected FileStatus statNullable(LocalPath path, boolean followSymlinks) {
     String name = path.getPathString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -199,16 +201,16 @@
   }
 
   @Override
-  protected boolean exists(Path path, boolean followSymlinks) {
+  protected boolean exists(LocalPath path, boolean followSymlinks) {
     return statNullable(path, followSymlinks) != null;
   }
 
   /**
-   * Return true iff the {@code stat} of {@code path} resulted in an {@code ENOENT}
-   * or {@code ENOTDIR} error.
+   * Return true iff the {@code stat} of {@code path} resulted in an {@code ENOENT} or {@code
+   * ENOTDIR} error.
    */
   @Override
-  protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus statIfFound(LocalPath path, boolean followSymlinks) throws IOException {
     String name = path.getPathString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -234,29 +236,29 @@
   }
 
   @Override
-  protected boolean isReadable(Path path) throws IOException {
+  protected boolean isReadable(LocalPath path) throws IOException {
     return (statInternal(path, true).getPermissions() & 0400) != 0;
   }
 
   @Override
-  protected boolean isWritable(Path path) throws IOException {
+  protected boolean isWritable(LocalPath path) throws IOException {
     return (statInternal(path, true).getPermissions() & 0200) != 0;
   }
 
   @Override
-  protected boolean isExecutable(Path path) throws IOException {
+  protected boolean isExecutable(LocalPath path) throws IOException {
     return (statInternal(path, true).getPermissions() & 0100) != 0;
   }
 
   /**
-   * Adds or remove the bits specified in "permissionBits" to the permission
-   * mask of the file specified by {@code path}. If the argument {@code add} is
-   * true, the specified permissions are added, otherwise they are removed.
+   * Adds or remove the bits specified in "permissionBits" to the permission mask of the file
+   * specified by {@code path}. If the argument {@code add} is true, the specified permissions are
+   * added, otherwise they are removed.
    *
    * @throws IOException if there was an error writing the file's metadata
    */
-  private void modifyPermissionBits(Path path, int permissionBits, boolean add)
-    throws IOException {
+  private void modifyPermissionBits(LocalPath path, int permissionBits, boolean add)
+      throws IOException {
     synchronized (path) {
       int oldMode = statInternal(path, true).getPermissions();
       int newMode = add ? (oldMode | permissionBits) : (oldMode & ~permissionBits);
@@ -265,39 +267,39 @@
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     modifyPermissionBits(path, 0400, readable);
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     modifyPermissionBits(path, 0200, writable);
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable) throws IOException {
+  protected void setExecutable(LocalPath path, boolean executable) throws IOException {
     modifyPermissionBits(path, 0111, executable);
   }
 
   @Override
-  protected void chmod(Path path, int mode) throws IOException {
+  protected void chmod(LocalPath path, int mode) throws IOException {
     synchronized (path) {
       NativePosixFiles.chmod(path.toString(), mode);
     }
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     return true;
   }
 
@@ -307,8 +309,10 @@
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
-    synchronized (path) {
+  public boolean createDirectory(LocalPath path) throws IOException {
+    Lock lock = getPathLock(path);
+    lock.lock();
+    try {
       // Note: UNIX mkdir(2), FilesystemUtils.mkdir() and createDirectory all
       // have different ways of representing failure!
       if (NativePosixFiles.mkdir(path.toString(), 0777)) {
@@ -321,25 +325,30 @@
       } else {
         throw new IOException(path + " (File exists)");
       }
+    } finally {
+      lock.unlock();
     }
   }
 
   @Override
-  protected void createSymbolicLink(Path linkPath, PathFragment targetFragment)
-      throws IOException {
-    synchronized (linkPath) {
-      NativePosixFiles.symlink(targetFragment.toString(), linkPath.toString());
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
+    Lock lock = getPathLock(linkPath);
+    lock.lock();
+    try {
+      NativePosixFiles.symlink(targetFragment, linkPath.toString());
+    } finally {
+      lock.unlock();
     }
   }
 
   @Override
-  protected PathFragment readSymbolicLink(Path path) throws IOException {
+  protected String readSymbolicLink(LocalPath path) throws IOException {
     // Note that the default implementation of readSymbolicLinkUnchecked calls this method and thus
     // is optimal since we only make one system call in here.
     String name = path.toString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
-      return PathFragment.create(NativePosixFiles.readlink(name));
+      return NativePosixFiles.readlink(name);
     } catch (IOException e) {
       // EINVAL => not a symbolic link.  Anything else is a real error.
       throw e.getMessage().endsWith("(Invalid argument)") ? new NotASymlinkException(path) : e;
@@ -349,38 +358,45 @@
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath) throws IOException {
-    synchronized (sourcePath) {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
+    Lock lock = getPathLock(sourcePath);
+    lock.lock();
+    try {
       NativePosixFiles.rename(sourcePath.toString(), targetPath.toString());
+    } finally {
+      lock.unlock();
     }
   }
 
   @Override
-  protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+  protected long getFileSize(LocalPath path, boolean followSymlinks) throws IOException {
     return stat(path, followSymlinks).getSize();
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
+  public boolean delete(LocalPath path) throws IOException {
     String name = path.toString();
     long startTime = Profiler.nanoTimeMaybe();
-    synchronized (path) {
-      try {
-        return NativePosixFiles.remove(name);
-      } finally {
-        profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, name);
-      }
+    Lock lock = getPathLock(path);
+    lock.lock();
+    try {
+      return NativePosixFiles.remove(name);
+    } finally {
+      lock.unlock();
+      profiler.logSimpleTask(startTime, ProfilerTask.VFS_DELETE, name);
     }
   }
 
   @Override
-  protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+  protected long getLastModifiedTime(LocalPath path, boolean followSymlinks) throws IOException {
     return stat(path, followSymlinks).getLastModifiedTime();
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
-    synchronized (path) {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
+    Lock lock = getPathLock(path);
+    lock.lock();
+    try {
       if (newTime == -1L) { // "now"
         NativePosixFiles.utime(path.toString(), true, 0);
       } else {
@@ -388,11 +404,13 @@
         int unixTime = (int) (newTime / 1000);
         NativePosixFiles.utime(path.toString(), false, unixTime);
       }
+    } finally {
+      lock.unlock();
     }
   }
 
   @Override
-  public byte[] getxattr(Path path, String name) throws IOException {
+  public byte[] getxattr(LocalPath path, String name) throws IOException {
     String pathName = path.toString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -407,7 +425,7 @@
   }
 
   @Override
-  protected byte[] getDigest(Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     String name = path.toString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -421,8 +439,13 @@
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException {
     NativePosixFiles.link(originalPath.toString(), linkPath.toString());
   }
+
+  /** Returns a per-path lock. The lock is re-entrant. */
+  protected Lock getPathLock(LocalPath path) {
+    return pathLock.get(path);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
index ef1946b..8b9cd53 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystem.java
@@ -37,7 +37,7 @@
   }
 
   @Override
-  protected InputStream getInputStream(Path path) throws IOException {
+  protected InputStream getInputStream(LocalPath path) throws IOException {
     // This loop is a workaround for an apparent bug in FileInputStream.open, which delegates
     // ultimately to JVM_Open in the Hotspot JVM.  This call is not EINTR-safe, so we must do the
     // retry here.
@@ -55,7 +55,7 @@
   }
 
   /** Returns either normal or profiled FileInputStream. */
-  private InputStream createFileInputStream(Path path) throws FileNotFoundException {
+  private InputStream createFileInputStream(LocalPath path) throws FileNotFoundException {
     final String name = path.toString();
     if (profiler.isActive()
         && (profiler.isProfiling(ProfilerTask.VFS_READ)
@@ -77,7 +77,7 @@
    * Returns either normal or profiled FileOutputStream. Should be used by subclasses to create
    * default OutputStream instance.
    */
-  protected OutputStream createFileOutputStream(Path path, boolean append)
+  protected OutputStream createFileOutputStream(LocalPath path, boolean append)
       throws FileNotFoundException {
     final String name = path.toString();
     if (profiler.isActive()
@@ -95,7 +95,7 @@
   }
 
   @Override
-  protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+  protected OutputStream getOutputStream(LocalPath path, boolean append) throws IOException {
     synchronized (path) {
       try {
         return createFileOutputStream(path, append);
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystemWithCustomStat.java b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystemWithCustomStat.java
index 875df98..b73aa0c 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystemWithCustomStat.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/AbstractFileSystemWithCustomStat.java
@@ -29,30 +29,30 @@
   }
 
   @Override
-  protected boolean isFile(Path path, boolean followSymlinks) {
+  protected boolean isFile(LocalPath path, boolean followSymlinks) {
     FileStatus stat = statNullable(path, followSymlinks);
     return stat != null ? stat.isFile() : false;
   }
 
   @Override
-  protected boolean isSpecialFile(Path path, boolean followSymlinks) {
+  protected boolean isSpecialFile(LocalPath path, boolean followSymlinks) {
     FileStatus stat = statNullable(path, followSymlinks);
     return stat != null ? stat.isSpecialFile() : false;
   }
 
   @Override
-  protected boolean isSymbolicLink(Path path) {
+  protected boolean isSymbolicLink(LocalPath path) {
     FileStatus stat = statNullable(path, false);
     return stat != null ? stat.isSymbolicLink() : false;
   }
 
   @Override
-  protected boolean isDirectory(Path path, boolean followSymlinks) {
+  protected boolean isDirectory(LocalPath path, boolean followSymlinks) {
     FileStatus stat = statNullable(path, followSymlinks);
     return stat != null ? stat.isDirectory() : false;
   }
 
   @Override
-  protected abstract FileStatus stat(Path path, boolean followSymlinks) throws IOException;
+  protected abstract FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException;
 }
 
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 fef88b8..0e92b3d 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
@@ -39,7 +39,6 @@
  */
 @ThreadSafe
 public abstract class FileSystem {
-
   /** Type of hash function to use for digesting files. */
   // The underlying HashFunctions are immutable and thread safe.
   @SuppressWarnings("ImmutableEnumChecker")
@@ -100,15 +99,15 @@
       public Path getCachedChildPathInternal(Path path, String childName) {
         return Path.getCachedChildPathInternal(path, childName, /*cacheable=*/ true);
       }
-    };
+    }
   }
 
   /**
    * An exception thrown when attempting to resolve an ordinary file as a symlink.
    */
   protected static final class NotASymlinkException extends IOException {
-    public NotASymlinkException(Path path) {
-      super(path.toString());
+    public NotASymlinkException(LocalPath path) {
+      super(path.getPathString());
     }
   }
 
@@ -121,24 +120,22 @@
   }
 
   /**
-   * Returns an absolute path instance, given an absolute path name, without
-   * double slashes, .., or . segments. While this method will normalize the
-   * path representation by creating a structured/parsed representation, it will
-   * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
-   * file system.
+   * Returns an absolute path instance, given an absolute path name, without double slashes, .., or
+   * . segments. While this method will normalize the path representation by creating a
+   * structured/parsed representation, it will not cause any IO. (e.g., it will not resolve symbolic
+   * links if it's a Unix file system.
    */
   public Path getPath(String pathName) {
     return getPath(PathFragment.create(pathName));
   }
 
   /**
-   * Returns an absolute path instance, given an absolute path name, without
-   * double slashes, .., or . segments. While this method will normalize the
-   * path representation by creating a structured/parsed representation, it will
-   * not cause any IO. (e.g., it will not resolve symbolic links if it's a Unix
-   * file system.
+   * Returns an absolute path instance, given an absolute path name, without double slashes, .., or
+   * . segments. While this method will normalize the path representation by creating a
+   * structured/parsed representation, it will not cause any IO. (e.g., it will not resolve symbolic
+   * links if it's a Unix file system.
    */
-  public Path getPath(PathFragment pathName) {
+  public final Path getPath(PathFragment pathName) {
     if (!pathName.isAbsolute()) {
       throw new IllegalArgumentException(pathName.getPathString()  + " (not an absolute path)");
     }
@@ -165,14 +162,14 @@
    * <p>Returns true if FileSystem supports the following:
    *
    * <ul>
-   *   <li>{@link #setWritable(Path, boolean)}
-   *   <li>{@link #setExecutable(Path, boolean)}
+   *   <li>{@link #setWritable(LocalPath, boolean)}
+   *   <li>{@link #setExecutable(LocalPath, boolean)}
    * </ul>
    *
    * The above calls will result in an {@link UnsupportedOperationException} on a FileSystem where
    * this method returns {@code false}.
    */
-  public abstract boolean supportsModifications(Path path);
+  public abstract boolean supportsModifications(LocalPath path);
 
   /**
    * Returns whether or not the FileSystem supports symbolic links.
@@ -180,17 +177,17 @@
    * <p>Returns true if FileSystem supports the following:
    *
    * <ul>
-   *   <li>{@link #createSymbolicLink(Path, PathFragment)}
-   *   <li>{@link #getFileSize(Path, boolean)} where {@code followSymlinks=false}
-   *   <li>{@link #getLastModifiedTime(Path, boolean)} where {@code followSymlinks=false}
-   *   <li>{@link #readSymbolicLink(Path)} where the link points to a non-existent file
+   *   <li>{@link #createSymbolicLink(LocalPath, String)}
+   *   <li>{@link #getFileSize(LocalPath, boolean)} where {@code followSymlinks=false}
+   *   <li>{@link #getLastModifiedTime(LocalPath, boolean)} where {@code followSymlinks=false}
+   *   <li>{@link #readSymbolicLink(LocalPath)} where the link points to a non-existent file
    * </ul>
    *
    * The above calls may result in an {@link UnsupportedOperationException} on a FileSystem where
    * this method returns {@code false}. The implementation can try to emulate these calls at its own
    * discretion.
    */
-  public abstract boolean supportsSymbolicLinksNatively(Path path);
+  public abstract boolean supportsSymbolicLinksNatively(LocalPath path);
 
   /**
    * Returns whether or not the FileSystem supports hard links.
@@ -198,14 +195,14 @@
    * <p>Returns true if FileSystem supports the following:
    *
    * <ul>
-   *   <li>{@link #createFSDependentHardLink(Path, Path)}
+   *   <li>{@link #createFSDependentHardLink(LocalPath, LocalPath)}
    * </ul>
    *
    * The above calls may result in an {@link UnsupportedOperationException} on a FileSystem where
    * this method returns {@code false}. The implementation can try to emulate these calls at its own
    * discretion.
    */
-  protected abstract boolean supportsHardLinksNatively(Path path);
+  protected abstract boolean supportsHardLinksNatively(LocalPath path);
 
   /***
    * Returns true if file path is case-sensitive on this file system. Default is true.
@@ -215,28 +212,27 @@
   /**
    * Returns the type of the file system path belongs to.
    *
-   * <p>The string returned is obtained directly from the operating system, so
-   * it's a best guess in absence of a guaranteed api.
+   * <p>The string returned is obtained directly from the operating system, so it's a best guess in
+   * absence of a guaranteed api.
    *
-   * <p>This implementation uses <code>/proc/mounts</code> to determine the
-   * file system type.
+   * <p>This implementation uses <code>/proc/mounts</code> to determine the file system type.
    */
-  public String getFileSystemType(Path path) {
+  public String getFileSystemType(LocalPath path) {
     String fileSystem = "unknown";
     int bestMountPointSegmentCount = -1;
     try {
-      Path canonicalPath = path.resolveSymbolicLinks();
-      Path mountTable = path.getRelative("/proc/mounts");
-      try (InputStreamReader reader = new InputStreamReader(mountTable.getInputStream(),
-          ISO_8859_1)) {
+      LocalPath canonicalPath = resolveSymbolicLinks(path);
+      LocalPath mountTable = path.getRelative("/proc/mounts");
+      try (InputStreamReader reader =
+          new InputStreamReader(getInputStream(mountTable), ISO_8859_1)) {
         for (String line : CharStreams.readLines(reader)) {
           String[] words = line.split("\\s+");
           if (words.length >= 3) {
             if (!words[1].startsWith("/")) {
               continue;
             }
-            Path mountPoint = path.getFileSystem().getPath(words[1]);
-            int segmentCount = mountPoint.asFragment().segmentCount();
+            LocalPath mountPoint = LocalPath.create(words[1]);
+            int segmentCount = mountPoint.split().size();
             if (canonicalPath.startsWith(mountPoint) && segmentCount > bestMountPointSegmentCount) {
               bestMountPointSegmentCount = segmentCount;
               fileSystem = words[2];
@@ -254,62 +250,62 @@
    * Creates a directory with the name of the current path. See {@link Path#createDirectory} for
    * specification.
    */
-  public abstract boolean createDirectory(Path path) throws IOException;
+  public abstract boolean createDirectory(LocalPath path) throws IOException;
 
   /**
    * Returns the size in bytes of the file denoted by {@code path}. See {@link
    * Path#getFileSize(Symlinks)} for specification.
    *
-   * <p>Note: for <@link FileSystem>s where {@link #supportsSymbolicLinksNatively(Path)} returns
-   * false, this method will throw an {@link UnsupportedOperationException} if {@code
+   * <p>Note: for <@link FileSystem>s where {@link #supportsSymbolicLinksNatively(LocalPath)}
+   * returns false, this method will throw an {@link UnsupportedOperationException} if {@code
    * followSymLinks=false}.
    */
-  protected abstract long getFileSize(Path path, boolean followSymlinks) throws IOException;
+  protected abstract long getFileSize(LocalPath path, boolean followSymlinks) throws IOException;
 
   /** Deletes the file denoted by {@code path}. See {@link Path#delete} for specification. */
-  public abstract boolean delete(Path path) throws IOException;
+  public abstract boolean delete(LocalPath path) throws IOException;
 
   /**
    * Returns the last modification time of the file denoted by {@code path}. See {@link
    * Path#getLastModifiedTime(Symlinks)} for specification.
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(Path)} returns
-   * false, this method will throw an {@link UnsupportedOperationException} if {@code
+   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(LocalPath)}
+   * returns false, this method will throw an {@link UnsupportedOperationException} if {@code
    * followSymLinks=false}.
    */
-  protected abstract long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException;
+  protected abstract long getLastModifiedTime(LocalPath path, boolean followSymlinks)
+      throws IOException;
 
   /**
    * Sets the last modification time of the file denoted by {@code path}. See {@link
    * Path#setLastModifiedTime} for specification.
    */
-  public abstract void setLastModifiedTime(Path path, long newTime) throws IOException;
+  public abstract void setLastModifiedTime(LocalPath path, long newTime) throws IOException;
 
   /**
-   * Returns value of the given extended attribute name or null if attribute
-   * does not exist or file system does not support extended attributes. Follows symlinks.
-   * <p>Default implementation assumes that file system does not support
-   * extended attributes and always returns null. Specific file system
-   * implementations should override this method if they do provide support
-   * for extended attributes.
+   * Returns value of the given extended attribute name or null if attribute does not exist or file
+   * system does not support extended attributes. Follows symlinks.
+   *
+   * <p>Default implementation assumes that file system does not support extended attributes and
+   * always returns null. Specific file system implementations should override this method if they
+   * do provide support for extended attributes.
    *
    * @param path the file whose extended attribute is to be returned.
    * @param name the name of the extended attribute key.
-   * @return the value of the extended attribute associated with 'path', if
-   *   any, or null if no such attribute is defined (ENODATA) or file
-   *   system does not support extended attributes at all.
+   * @return the value of the extended attribute associated with 'path', if any, or null if no such
+   *     attribute is defined (ENODATA) or file system does not support extended attributes at all.
    * @throws IOException if the call failed for any other reason.
    */
-  public byte[] getxattr(Path path, String name) throws IOException {
+  public byte[] getxattr(LocalPath path, String name) throws IOException {
     return null;
   }
 
   /**
-   * Gets a fast digest for the given path and hash function type, or {@code null} if there
-   * isn't one available or the filesystem doesn't support them. This digest should be
-   * suitable for detecting changes to the file.
+   * Gets a fast digest for the given path and hash function type, or {@code null} if there isn't
+   * one available or the filesystem doesn't support them. This digest should be suitable for
+   * detecting changes to the file.
    */
-  protected byte[] getFastDigest(Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getFastDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     return null;
   }
 
@@ -318,7 +314,7 @@
    * filesystem doesn't support them. This digest should be suitable for detecting changes to the
    * file.
    */
-  protected final byte[] getFastDigest(Path path) throws IOException {
+  protected final byte[] getFastDigest(LocalPath path) throws IOException {
     return getFastDigest(path, digestFunction);
   }
 
@@ -330,15 +326,14 @@
   }
 
   /**
-   * Returns the digest of the file denoted by the path, following
-   * symbolic links, for the given hash digest function.
+   * Returns the digest of the file denoted by the path, following symbolic links, for the given
+   * hash digest function.
    *
    * @return a new byte array containing the file's digest
    * @throws IOException if the digest could not be computed for any reason
-   *
-   * Subclasses may (and do) optimize this computation for particular digest functions.
+   *     <p>Subclasses may (and do) optimize this computation for particular digest functions.
    */
-  protected byte[] getDigest(final Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     return new ByteSource() {
       @Override
       public InputStream openStream() throws IOException {
@@ -353,34 +348,33 @@
    * @return a new byte array containing the file's digest
    * @throws IOException if the digest could not be computed for any reason
    */
-  protected final byte[] getDigest(final Path path) throws IOException {
+  protected final byte[] getDigest(LocalPath path) throws IOException {
     return getDigest(path, digestFunction);
   }
 
   /**
-   * Returns true if "path" denotes an existing symbolic link. See
-   * {@link Path#isSymbolicLink} for specification.
+   * Returns true if "path" denotes an existing symbolic link. See {@link Path#isSymbolicLink} for
+   * specification.
    */
-  protected abstract boolean isSymbolicLink(Path path);
+  protected abstract boolean isSymbolicLink(LocalPath path);
 
   /**
-   * Appends a single regular path segment 'child' to 'dir', recursively
-   * resolving symbolic links in 'child'. 'dir' must be canonical. 'maxLinks' is
-   * the maximum number of symbolic links that may be traversed before it gives
-   * up (the Linux kernel uses 32).
+   * Appends a single regular path segment 'child' to 'dir', recursively resolving symbolic links in
+   * 'child'. 'dir' must be canonical. 'maxLinks' is the maximum number of symbolic links that may
+   * be traversed before it gives up (the Linux kernel uses 32).
    *
-   * <p>(This method does not need to be synchronized; but the result may be
-   * stale in the case of concurrent modification.)
+   * <p>(This method does not need to be synchronized; but the result may be stale in the case of
+   * concurrent modification.)
    *
-   * @throws IOException if 'dir' is not an existing directory; or if
-   *         stat(child) fails for any reason, or if 'child' is a symlink and
-   *         readlink(child) fails for any reason (e.g. ENOENT, EACCES), or if
-   *         the chain of symbolic links exceeds 'maxLinks'.
+   * @throws IOException if 'dir' is not an existing directory; or if stat(child) fails for any
+   *     reason, or if 'child' is a symlink and readlink(child) fails for any reason (e.g. ENOENT,
+   *     EACCES), or if the chain of symbolic links exceeds 'maxLinks'.
    */
-  protected final Path appendSegment(Path dir, String child, int maxLinks) throws IOException {
-    Path naive = dir.getChild(child);
+  protected final LocalPath appendSegment(LocalPath dir, String child, int maxLinks)
+      throws IOException {
+    LocalPath naive = dir.getRelative(child);
 
-    PathFragment linkTarget = resolveOneLink(naive);
+    String linkTarget = resolveOneLink(naive);
     if (linkTarget == null) {
       return naive; // regular file or directory
     }
@@ -388,14 +382,15 @@
     if (maxLinks-- == 0) {
       throw new IOException(naive + " (Too many levels of symbolic links)");
     }
-    if (linkTarget.isAbsolute()) {
-      dir = getRootDirectory();
+    LocalPath linkTargetPath = LocalPath.create(linkTarget);
+    if (linkTargetPath.isAbsolute()) {
+      dir = linkTargetPath.getDrive();
     }
-    for (String name : linkTarget.segments()) {
+    for (String name : linkTargetPath.split()) {
       if (name.equals(".") || name.isEmpty()) {
         // no-op
       } else if (name.equals("..")) {
-        Path parent = dir.getParentDirectory();
+        LocalPath parent = dir.getParentDirectory();
         // root's parent is root, when canonicalizing, so this is a no-op.
         if (parent != null) { dir = parent; }
       } else {
@@ -406,21 +401,19 @@
   }
 
   /**
-   * Helper method of {@link #resolveSymbolicLinks(Path)}. This method
-   * encapsulates the I/O component of a full canonicalization operation.
-   * Subclasses can (and do) provide more efficient implementations.
+   * Helper method of {@link #resolveSymbolicLinks(LocalPath)}. This method encapsulates the I/O
+   * component of a full canonicalization operation. Subclasses can (and do) provide more efficient
+   * implementations.
    *
-   * <p>(This method does not need to be synchronized; but the result may be
-   * stale in the case of concurrent modification.)
+   * <p>(This method does not need to be synchronized; but the result may be stale in the case of
+   * concurrent modification.)
    *
-   * @param path a path, of which all but the last segment is guaranteed to be
-   *        canonical
-   * @return {@link #readSymbolicLink} iff path is a symlink or null iff
-   *         path exists but is not a symlink
-   * @throws IOException if the file did not exist, or a parent directory could
-   *         not be searched
+   * @param path a path, of which all but the last segment is guaranteed to be canonical
+   * @return {@link #readSymbolicLink} iff path is a symlink or null iff path exists but is not a
+   *     symlink
+   * @throws IOException if the file did not exist, or a parent directory could not be searched
    */
-  protected PathFragment resolveOneLink(Path path) throws IOException {
+  protected String resolveOneLink(LocalPath path) throws IOException {
     try {
       return readSymbolicLink(path);
     } catch (NotASymlinkException e) {
@@ -440,28 +433,25 @@
   }
 
   /**
-   * Returns the canonical path for the given path. See
-   * {@link Path#resolveSymbolicLinks} for specification.
+   * Returns the canonical path for the given path. See {@link Path#resolveSymbolicLinks} for
+   * specification.
    */
-  protected Path resolveSymbolicLinks(Path path)
-      throws IOException {
-    Path parentNode = path.getParentDirectory();
+  protected LocalPath resolveSymbolicLinks(LocalPath path) throws IOException {
+    LocalPath parentNode = path.getParentDirectory();
     return parentNode == null
         ? path // (root)
         : appendSegment(resolveSymbolicLinks(parentNode), path.getBaseName(), 32);
   }
 
   /**
-   * Returns the status of a file. See {@link Path#stat(Symlinks)} for
-   * specification.
+   * Returns the status of a file. See {@link Path#stat(Symlinks)} for specification.
    *
-   * <p>The default implementation of this method is a "lazy" one, based on
-   * other accessor methods such as {@link #isFile}, etc. Subclasses may provide
-   * more efficient specializations. However, we still try to follow Unix-like
-   * semantics of failing fast in case of non-existent files (or in case of
-   * permission issues).
+   * <p>The default implementation of this method is a "lazy" one, based on other accessor methods
+   * such as {@link #isFile}, etc. Subclasses may provide more efficient specializations. However,
+   * we still try to follow Unix-like semantics of failing fast in case of non-existent files (or in
+   * case of permission issues).
    */
-  protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
+  protected FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException {
     FileStatus status = new FileStatus() {
       volatile Boolean isFile;
       volatile Boolean isDirectory;
@@ -526,10 +516,8 @@
     return status;
   }
 
-  /**
-   * Like stat(), but returns null on failures instead of throwing.
-   */
-  protected FileStatus statNullable(Path path, boolean followSymlinks) {
+  /** Like stat(), but returns null on failures instead of throwing. */
+  protected FileStatus statNullable(LocalPath path, boolean followSymlinks) {
     try {
       return stat(path, followSymlinks);
     } catch (IOException e) {
@@ -538,12 +526,12 @@
   }
 
   /**
-   * Like {@link #stat}, but returns null if the file is not found (corresponding to
-   * {@code ENOENT} or {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Note that
-   * this implementation does <i>not</i> successfully catch {@code ENOTDIR} exceptions. If the
+   * Like {@link #stat}, but returns null if the file is not found (corresponding to {@code ENOENT}
+   * or {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Note that this
+   * implementation does <i>not</i> successfully catch {@code ENOTDIR} exceptions. If the
    * instantiated filesystem can catch such errors, it should override this method to do so.
    */
-  protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus statIfFound(LocalPath path, boolean followSymlinks) throws IOException {
     try {
       return stat(path, followSymlinks);
     } catch (FileNotFoundException e) {
@@ -552,65 +540,65 @@
   }
 
   /**
-   * Returns true iff {@code path} denotes an existing directory. See
-   * {@link Path#isDirectory(Symlinks)} for specification.
+   * Returns true iff {@code path} denotes an existing directory. See {@link
+   * Path#isDirectory(Symlinks)} for specification.
    */
-  protected abstract boolean isDirectory(Path path, boolean followSymlinks);
+  protected abstract boolean isDirectory(LocalPath path, boolean followSymlinks);
 
   /**
-   * Returns true iff {@code path} denotes an existing regular or special file.
-   * See {@link Path#isFile(Symlinks)} for specification.
+   * Returns true iff {@code path} denotes an existing regular or special file. See {@link
+   * Path#isFile(Symlinks)} for specification.
    */
-  protected abstract boolean isFile(Path path, boolean followSymlinks);
+  protected abstract boolean isFile(LocalPath path, boolean followSymlinks);
 
   /**
-   * Returns true iff {@code path} denotes a special file.
-   * See {@link Path#isSpecialFile(Symlinks)} for specification.
+   * Returns true iff {@code path} denotes a special file. See {@link Path#isSpecialFile(Symlinks)}
+   * for specification.
    */
-  protected abstract boolean isSpecialFile(Path path, boolean followSymlinks);
+  protected abstract boolean isSpecialFile(LocalPath path, boolean followSymlinks);
 
   /**
    * Creates a symbolic link. See {@link Path#createSymbolicLink(Path)} for specification.
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(Path)} returns
-   * false, this method will throw an {@link UnsupportedOperationException}
+   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(LocalPath)}
+   * returns false, this method will throw an {@link UnsupportedOperationException}
    */
-  protected abstract void createSymbolicLink(Path linkPath, PathFragment targetFragment)
+  protected abstract void createSymbolicLink(LocalPath linkPath, String targetFragment)
       throws IOException;
 
   /**
    * Returns the target of a symbolic link. See {@link Path#readSymbolicLink} for specification.
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(Path)} returns
-   * false, this method will throw an {@link UnsupportedOperationException} if the link points to a
-   * non-existent file.
+   * <p>Note: for {@link FileSystem}s where {@link #supportsSymbolicLinksNatively(LocalPath)}
+   * returns false, this method will throw an {@link UnsupportedOperationException} if the link
+   * points to a non-existent file.
    *
    * @throws NotASymlinkException if the current path is not a symbolic link
    * @throws IOException if the contents of the link could not be read for any reason.
    */
-  protected abstract PathFragment readSymbolicLink(Path path) throws IOException;
+  protected abstract String readSymbolicLink(LocalPath path) throws IOException;
 
   /**
    * Returns the target of a symbolic link, under the assumption that the given path is indeed a
-   * symbolic link (this assumption permits efficient implementations). See
-   * {@link Path#readSymbolicLinkUnchecked} for specification.
+   * symbolic link (this assumption permits efficient implementations). See {@link
+   * Path#readSymbolicLinkUnchecked} for specification.
    *
    * @throws IOException if the contents of the link could not be read for any reason.
    */
-  protected PathFragment readSymbolicLinkUnchecked(Path path) throws IOException {
+  protected String readSymbolicLinkUnchecked(LocalPath path) throws IOException {
     return readSymbolicLink(path);
   }
 
   /** Returns true iff this path denotes an existing file of any kind. Follows symbolic links. */
-  public boolean exists(Path path) {
+  public boolean exists(LocalPath path) {
     return exists(path, true);
   }
 
   /**
-   * Returns true iff {@code path} denotes an existing file of any kind. See
-   * {@link Path#exists(Symlinks)} for specification.
+   * Returns true iff {@code path} denotes an existing file of any kind. See {@link
+   * Path#exists(Symlinks)} for specification.
    */
-  protected abstract boolean exists(Path path, boolean followSymlinks);
+  protected abstract boolean exists(LocalPath path, boolean followSymlinks);
 
   /**
    * Returns a collection containing the names of all entities within the directory denoted by the
@@ -618,7 +606,7 @@
    *
    * @throws IOException if there was an error reading the directory entries
    */
-  protected abstract Collection<String> getDirectoryEntries(Path path) throws IOException;
+  protected abstract Collection<String> getDirectoryEntries(LocalPath path) throws IOException;
 
   protected static Dirent.Type direntFromStat(FileStatus stat) {
     if (stat == null) {
@@ -637,19 +625,19 @@
   }
 
   /**
-   * Returns a Dirents structure, listing the names of all entries within the
-   * directory {@code path}, plus their types (file, directory, other).
+   * Returns a Dirents structure, listing the names of all entries within the directory {@code
+   * path}, plus their types (file, directory, other).
    *
-   * @param followSymlinks whether to follow symlinks when determining the file types of
-   *     individual directory entries. No matter the value of this parameter, symlinks are
-   *     followed when resolving the directory whose entries are to be read.
+   * @param followSymlinks whether to follow symlinks when determining the file types of individual
+   *     directory entries. No matter the value of this parameter, symlinks are followed when
+   *     resolving the directory whose entries are to be read.
    * @throws IOException if there was an error reading the directory entries
    */
-  protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+  protected Collection<Dirent> readdir(LocalPath path, boolean followSymlinks) throws IOException {
     Collection<String> children = getDirectoryEntries(path);
     List<Dirent> dirents = Lists.newArrayListWithCapacity(children.size());
     for (String child : children) {
-      Path childPath = path.getChild(child);
+      LocalPath childPath = path.getRelative(child);
       Dirent.Type type = direntFromStat(statNullable(childPath, followSymlinks));
       dirents.add(new Dirent(child, type));
     }
@@ -661,54 +649,54 @@
    *
    * @throws IOException if there was an error reading the file's metadata
    */
-  protected abstract boolean isReadable(Path path) throws IOException;
+  protected abstract boolean isReadable(LocalPath path) throws IOException;
 
   /**
    * Sets the file to readable (if the argument is true) or non-readable (if the argument is false)
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(Path)} returns false or
-   * which do not support unreadable files, this method will throw an {@link
+   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(LocalPath)} returns false
+   * or which do not support unreadable files, this method will throw an {@link
    * UnsupportedOperationException}.
    *
    * @throws IOException if there was an error reading or writing the file's metadata
    */
-  protected abstract void setReadable(Path path, boolean readable) throws IOException;
+  protected abstract void setReadable(LocalPath path, boolean readable) throws IOException;
 
   /**
    * Returns true iff the file represented by {@code path} is writable.
    *
    * @throws IOException if there was an error reading the file's metadata
    */
-  protected abstract boolean isWritable(Path path) throws IOException;
+  protected abstract boolean isWritable(LocalPath path) throws IOException;
 
   /**
    * Sets the file to writable (if the argument is true) or non-writable (if the argument is false)
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(Path)} returns false, this
-   * method will throw an {@link UnsupportedOperationException}.
+   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(LocalPath)} returns false,
+   * this method will throw an {@link UnsupportedOperationException}.
    *
    * @throws IOException if there was an error reading or writing the file's metadata
    */
-  public abstract void setWritable(Path path, boolean writable) throws IOException;
+  public abstract void setWritable(LocalPath path, boolean writable) throws IOException;
 
   /**
    * Returns true iff the file represented by the path is executable.
    *
    * @throws IOException if there was an error reading the file's metadata
    */
-  protected abstract boolean isExecutable(Path path) throws IOException;
+  protected abstract boolean isExecutable(LocalPath path) throws IOException;
 
   /**
    * Sets the file to executable, if the argument is true. It is currently not supported to unset
    * the executable status of a file, so {code executable=false} yields an {@link
    * UnsupportedOperationException}.
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(Path)} returns false, this
-   * method will throw an {@link UnsupportedOperationException}.
+   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(LocalPath)} returns false,
+   * this method will throw an {@link UnsupportedOperationException}.
    *
    * @throws IOException if there was an error reading or writing the file's metadata
    */
-  protected abstract void setExecutable(Path path, boolean executable) throws IOException;
+  protected abstract void setExecutable(LocalPath path, boolean executable) throws IOException;
 
   /**
    * Sets the file permissions. If permission changes on this {@link FileSystem} are slow (e.g. one
@@ -716,12 +704,12 @@
    * individually. If this {@link FileSystem} does not support group or others permissions, those
    * bits will be ignored.
    *
-   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(Path)} returns false, this
-   * method will throw an {@link UnsupportedOperationException}.
+   * <p>Note: for {@link FileSystem}s where {@link #supportsModifications(LocalPath)} returns false,
+   * this method will throw an {@link UnsupportedOperationException}.
    *
    * @throws IOException if there was an error reading or writing the file's metadata
    */
-  protected void chmod(Path path, int mode) throws IOException {
+  protected void chmod(LocalPath path, int mode) throws IOException {
     setReadable(path, (mode & 0400) != 0);
     setWritable(path, (mode & 0200) != 0);
     setExecutable(path, (mode & 0100) != 0);
@@ -732,14 +720,14 @@
    *
    * @throws IOException if there was an error opening the file for reading
    */
-  protected abstract InputStream getInputStream(Path path) throws IOException;
+  protected abstract InputStream getInputStream(LocalPath path) throws IOException;
 
   /**
    * Creates an OutputStream accessing the file denoted by path.
    *
    * @throws IOException if there was an error opening the file for writing
    */
-  protected final OutputStream getOutputStream(Path path) throws IOException {
+  protected final OutputStream getOutputStream(LocalPath path) throws IOException {
     return getOutputStream(path, false);
   }
 
@@ -749,13 +737,14 @@
    * @param append whether to open the output stream in append mode
    * @throws IOException if there was an error opening the file for writing
    */
-  protected abstract OutputStream getOutputStream(Path path, boolean append) throws IOException;
+  protected abstract OutputStream getOutputStream(LocalPath path, boolean append)
+      throws IOException;
 
   /**
    * Renames the file denoted by "sourceNode" to the location "targetNode". See {@link
    * Path#renameTo} for specification.
    */
-  public abstract void renameTo(Path sourcePath, Path targetPath) throws IOException;
+  public abstract void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException;
 
   /**
    * Create a new hard link file at "linkPath" for file at "originalPath".
@@ -764,9 +753,9 @@
    * @param originalPath The path of the original file
    * @throws IOException if the original file does not exist or the link file already exists
    */
-  protected void createHardLink(Path linkPath, Path originalPath) throws IOException {
+  protected void createHardLink(LocalPath linkPath, LocalPath originalPath) throws IOException {
 
-    if (!originalPath.exists()) {
+    if (!exists(originalPath, true)) {
       throw new FileNotFoundException(
           "File \""
               + originalPath.getBaseName()
@@ -775,7 +764,7 @@
               + "\" does not exist");
     }
 
-    if (linkPath.exists()) {
+    if (exists(linkPath, true)) {
       throw new FileAlreadyExistsException(
           "New link file \"" + linkPath.getBaseName() + "\" already exists");
     }
@@ -790,14 +779,13 @@
    * @param originalPath The path of the original file
    * @throws IOException if there was an I/O error
    */
-  protected abstract void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected abstract void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException;
 
   /**
-   * Prefetch all directories and symlinks within the package
-   * rooted at "path".  Enter at most "maxDirs" total directories.
-   * Specializations for high-latency remote filesystems may wish to
+   * Prefetch all directories and symlinks within the package rooted at "path". Enter at most
+   * "maxDirs" total directories. Specializations for high-latency remote filesystems may wish to
    * implement this in order to warm the filesystem's internal caches.
    */
-  protected void prefetchPackageAsync(Path path, int maxDirs) { }
+  protected void prefetchPackageAsync(LocalPath path, int maxDirs) {}
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
index 064e4b2..930a8be 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileSystemUtils.java
@@ -378,25 +378,39 @@
   }
 
   public static ByteSource asByteSource(final Path path) {
-    return new ByteSource() {
-      @Override public InputStream openStream() throws IOException {
-        return path.getInputStream();
-      }
-    };
+    return asByteSource(path.getFileSystem(), path.getLocalPath());
   }
 
   public static ByteSink asByteSink(final Path path, final boolean append) {
-    return new ByteSink() {
-      @Override public OutputStream openStream() throws IOException {
-        return path.getOutputStream(append);
-      }
-    };
+    return asByteSink(path.getFileSystem(), path.getLocalPath(), append);
   }
 
   public static ByteSink asByteSink(final Path path) {
     return asByteSink(path, false);
   }
 
+  public static ByteSource asByteSource(FileSystem fileSystem, LocalPath path) {
+    return new ByteSource() {
+      @Override
+      public InputStream openStream() throws IOException {
+        return fileSystem.getInputStream(path);
+      }
+    };
+  }
+
+  public static ByteSink asByteSink(FileSystem fileSystem, LocalPath path, final boolean append) {
+    return new ByteSink() {
+      @Override
+      public OutputStream openStream() throws IOException {
+        return fileSystem.getOutputStream(path, append);
+      }
+    };
+  }
+
+  public static ByteSink asByteSink(FileSystem fileSystem, LocalPath path) {
+    return asByteSink(fileSystem, path, false);
+  }
+
   /**
    * Copies the file from location "from" to location "to", while overwriting a
    * potentially existing "to". File's last modified time, executable and
@@ -408,18 +422,34 @@
    */
   @ThreadSafe  // but not atomic
   public static void copyFile(Path from, Path to) throws IOException {
+    copyFile(from.getFileSystem(), from.getLocalPath(), to.getFileSystem(), to.getLocalPath());
+  }
+
+  /**
+   * Copies the file from location "from" to location "to", while overwriting a potentially existing
+   * "to". File's last modified time, executable and writable bits are also preserved.
+   *
+   * <p>If no error occurs, the method returns normally. If a parent directory does not exist, a
+   * FileNotFoundException is thrown. An IOException is thrown when other erroneous situations
+   * occur. (e.g. read errors)
+   */
+  @ThreadSafe // but not atomic
+  public static void copyFile(
+      FileSystem fromFileSystem, LocalPath from, FileSystem toFileSystem, LocalPath to)
+      throws IOException {
     try {
-      to.delete();
+      toFileSystem.delete(to);
     } catch (IOException e) {
       throw new IOException("error copying file: "
           + "couldn't delete destination: " + e.getMessage());
     }
-    asByteSource(from).copyTo(asByteSink(to));
-    to.setLastModifiedTime(from.getLastModifiedTime()); // Preserve mtime.
-    if (!from.isWritable()) {
-      to.setWritable(false); // Make file read-only if original was read-only.
+    asByteSource(fromFileSystem, from).copyTo(asByteSink(toFileSystem, to));
+    toFileSystem.setLastModifiedTime(
+        to, fromFileSystem.getLastModifiedTime(from, true)); // Preserve mtime.
+    if (!fromFileSystem.isWritable(from)) {
+      toFileSystem.setWritable(to, false); // Make file read-only if original was read-only.
     }
-    to.setExecutable(from.isExecutable()); // Copy executable bit.
+    toFileSystem.setExecutable(to, fromFileSystem.isExecutable(from)); // Copy executable bit.
   }
 
   /**
@@ -665,7 +695,7 @@
     if (filesystem instanceof UnionFileSystem) {
       // If using UnionFS, make sure that we do not traverse filesystem boundaries when creating
       // parent directories by rehoming the path on the most specific filesystem.
-      FileSystem delegate = ((UnionFileSystem) filesystem).getDelegate(dir);
+      FileSystem delegate = ((UnionFileSystem) filesystem).getDelegate(dir.getLocalPath());
       dir = delegate.getPath(dir.asFragment());
     }
 
@@ -757,7 +787,18 @@
    * @throws IOException if there was an error
    */
   public static void writeContentAsLatin1(Path outputFile, String content) throws IOException {
-    writeContent(outputFile, ISO_8859_1, content);
+    writeContentAsLatin1(outputFile.getFileSystem(), outputFile.getLocalPath(), content);
+  }
+
+  /**
+   * Writes the specified String as ISO-8859-1 (latin1) encoded bytes to the file. Follows symbolic
+   * links.
+   *
+   * @throws IOException if there was an error
+   */
+  public static void writeContentAsLatin1(
+      FileSystem fileSystem, LocalPath outputFile, String content) throws IOException {
+    writeContent(fileSystem, outputFile, ISO_8859_1, content);
   }
 
   /**
@@ -768,7 +809,18 @@
    */
   public static void writeContent(Path outputFile, Charset charset, String content)
       throws IOException {
-    asByteSink(outputFile).asCharSink(charset).write(content);
+    writeContent(outputFile.getFileSystem(), outputFile.getLocalPath(), charset, content);
+  }
+
+  /**
+   * Writes the specified String using the specified encoding to the file. Follows symbolic links.
+   *
+   * @throws IOException if there was an error
+   */
+  public static void writeContent(
+      FileSystem fileSystem, LocalPath outputFile, Charset charset, String content)
+      throws IOException {
+    asByteSink(fileSystem, outputFile).asCharSink(charset).write(content);
   }
 
   /**
@@ -977,7 +1029,7 @@
    * Returns the type of the file system path belongs to.
    */
   public static String getFileSystem(Path path) {
-    return path.getFileSystem().getFileSystemType(path);
+    return path.getFileSystem().getFileSystemType(path.getLocalPath());
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
index dca8b95..1381388 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/JavaIoFileSystem.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.vfs;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.Striped;
 import com.google.devtools.build.lib.clock.Clock;
 import com.google.devtools.build.lib.clock.JavaClock;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
@@ -28,6 +29,7 @@
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.concurrent.locks.Lock;
 
 /**
  * A FileSystem that does not use any JNI and hence, does not require a shared library be present at
@@ -39,6 +41,7 @@
  */
 @ThreadSafe
 public class JavaIoFileSystem extends AbstractFileSystemWithCustomStat {
+  private final Striped<Lock> pathLock = Striped.lock(64);
   private static final LinkOption[] NO_LINK_OPTION = new LinkOption[0];
   // This isn't generally safe; we rely on the file system APIs not modifying the array.
   private static final LinkOption[] NOFOLLOW_LINKS_OPTION =
@@ -66,7 +69,7 @@
     this.clock = clock;
   }
 
-  protected File getIoFile(Path path) {
+  protected File getIoFile(LocalPath path) {
     return new File(path.toString());
   }
 
@@ -78,7 +81,7 @@
    * avoids extra allocations and does not lose track of the underlying Java filesystem, which is
    * useful for some in-memory filesystem implementations like JimFS.
    */
-  protected java.nio.file.Path getNioPath(Path path) {
+  protected java.nio.file.Path getNioPath(LocalPath path) {
     return Paths.get(path.toString());
   }
 
@@ -87,7 +90,7 @@
   }
 
   @Override
-  protected Collection<String> getDirectoryEntries(Path path) throws IOException {
+  protected Collection<String> getDirectoryEntries(LocalPath path) throws IOException {
     File file = getIoFile(path);
     String[] entries = null;
     long startTime = Profiler.nanoTimeMaybe();
@@ -113,7 +116,7 @@
   }
 
   @Override
-  protected boolean exists(Path path, boolean followSymlinks) {
+  protected boolean exists(LocalPath path, boolean followSymlinks) {
     java.nio.file.Path nioPath = getNioPath(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -124,7 +127,7 @@
   }
 
   @Override
-  protected boolean isReadable(Path path) throws IOException {
+  protected boolean isReadable(LocalPath path) throws IOException {
     File file = getIoFile(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -138,7 +141,7 @@
   }
 
   @Override
-  protected boolean isWritable(Path path) throws IOException {
+  protected boolean isWritable(LocalPath path) throws IOException {
     File file = getIoFile(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -156,7 +159,7 @@
   }
 
   @Override
-  protected boolean isExecutable(Path path) throws IOException {
+  protected boolean isExecutable(LocalPath path) throws IOException {
     File file = getIoFile(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -170,7 +173,7 @@
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     File file = getIoFile(path);
     if (!file.exists()) {
       throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
@@ -179,7 +182,7 @@
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     File file = getIoFile(path);
     if (!file.exists()) {
       throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
@@ -188,7 +191,7 @@
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable) throws IOException {
+  protected void setExecutable(LocalPath path, boolean executable) throws IOException {
     File file = getIoFile(path);
     if (!file.exists()) {
       throw new FileNotFoundException(path + ERR_NO_SUCH_FILE_OR_DIR);
@@ -197,17 +200,17 @@
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     return true;
   }
 
@@ -217,22 +220,26 @@
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
+  public boolean createDirectory(LocalPath path) throws IOException {
 
     // We always synchronize on the current path before doing it on the parent path and file system
     // path structure ensures that this locking order will never be reversed.
     // When refactoring, check that subclasses still work as expected and there can be no
     // deadlocks.
-    synchronized (path) {
+    Lock lock = getPathLock(path);
+    lock.lock();
+    try {
       File file = getIoFile(path);
       if (file.mkdir()) {
         return true;
       }
 
       // We will be checking the state of the parent path as well. Synchronize on it before
-      // attempting anything.
-      Path parentDirectory = path.getParentDirectory();
-      synchronized (parentDirectory) {
+      // attempting anything. The striped lock used is re-entrant so this is safe.
+      LocalPath parentDirectory = path.getParentDirectory();
+      Lock parentLock = getPathLock(parentDirectory);
+      parentLock.lock();
+      try {
         if (fileIsSymbolicLink(file)) {
           throw new IOException(path + ERR_FILE_EXISTS);
         }
@@ -254,7 +261,11 @@
           // Parent exists, is writable, yet we can't create our directory.
           throw new FileNotFoundException(path.getParentDirectory() + ERR_NOT_A_DIRECTORY);
         }
+      } finally {
+        parentLock.unlock();
       }
+    } finally {
+      lock.unlock();
     }
   }
 
@@ -277,11 +288,10 @@
   }
 
   @Override
-  protected void createSymbolicLink(Path linkPath, PathFragment targetFragment)
-      throws IOException {
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
     java.nio.file.Path nioPath = getNioPath(linkPath);
     try {
-      Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getPathString()));
+      Files.createSymbolicLink(nioPath, Paths.get(targetFragment));
     } catch (java.nio.file.FileAlreadyExistsException e) {
       throw new IOException(linkPath + ERR_FILE_EXISTS);
     } catch (java.nio.file.AccessDeniedException e) {
@@ -292,12 +302,12 @@
   }
 
   @Override
-  protected PathFragment readSymbolicLink(Path path) throws IOException {
+  protected String readSymbolicLink(LocalPath path) throws IOException {
     java.nio.file.Path nioPath = getNioPath(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
       String link = Files.readSymbolicLink(nioPath).toString();
-      return PathFragment.create(link);
+      return link;
     } catch (java.nio.file.NotLinkException e) {
       throw new NotASymlinkException(path);
     } catch (java.nio.file.NoSuchFileException e) {
@@ -308,8 +318,10 @@
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath) throws IOException {
-    synchronized (sourcePath) {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
+    Lock lock = getPathLock(sourcePath);
+    lock.lock();
+    try {
       File sourceFile = getIoFile(sourcePath);
       File targetFile = getIoFile(targetPath);
       if (!sourceFile.renameTo(targetFile)) {
@@ -330,11 +342,13 @@
           throw new FileAccessException(sourcePath + " -> " + targetPath + ERR_PERMISSION_DENIED);
         }
       }
+    } finally {
+      lock.unlock();
     }
   }
 
   @Override
-  protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+  protected long getFileSize(LocalPath path, boolean followSymlinks) throws IOException {
     long startTime = Profiler.nanoTimeMaybe();
     try {
       return stat(path, followSymlinks).getSize();
@@ -344,7 +358,7 @@
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
+  public boolean delete(LocalPath path) throws IOException {
     File file = getIoFile(path);
     long startTime = Profiler.nanoTimeMaybe();
     synchronized (path) {
@@ -367,7 +381,7 @@
   }
 
   @Override
-  protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+  protected long getLastModifiedTime(LocalPath path, boolean followSymlinks) throws IOException {
     File file = getIoFile(path);
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -382,7 +396,7 @@
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
     File file = getIoFile(path);
     if (!file.setLastModified(newTime == -1L ? clock.currentTimeMillis() : newTime)) {
       if (!file.exists()) {
@@ -396,7 +410,7 @@
   }
 
   @Override
-  protected byte[] getDigest(Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     String name = path.toString();
     long startTime = Profiler.nanoTimeMaybe();
     try {
@@ -407,17 +421,15 @@
   }
 
   /**
-   * Returns the status of a file. See {@link Path#stat(Symlinks)} for
-   * specification.
+   * Returns the status of a file. See {@link Path#stat(Symlinks)} for specification.
    *
-   * <p>The default implementation of this method is a "lazy" one, based on
-   * other accessor methods such as {@link #isFile}, etc. Subclasses may provide
-   * more efficient specializations. However, we still try to follow Unix-like
-   * semantics of failing fast in case of non-existent files (or in case of
-   * permission issues).
+   * <p>The default implementation of this method is a "lazy" one, based on other accessor methods
+   * such as {@link #isFile}, etc. Subclasses may provide more efficient specializations. However,
+   * we still try to follow Unix-like semantics of failing fast in case of non-existent files (or in
+   * case of permission issues).
    */
   @Override
-  protected FileStatus stat(final Path path, final boolean followSymlinks) throws IOException {
+  protected FileStatus stat(LocalPath path, final boolean followSymlinks) throws IOException {
     java.nio.file.Path nioPath = getNioPath(path);
     final BasicFileAttributes attributes;
     try {
@@ -474,7 +486,7 @@
   }
 
   @Override
-  protected FileStatus statIfFound(Path path, boolean followSymlinks) {
+  protected FileStatus statIfFound(LocalPath path, boolean followSymlinks) {
     try {
       return stat(path, followSymlinks);
     } catch (FileNotFoundException e) {
@@ -491,10 +503,15 @@
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException {
     Files.createLink(
         java.nio.file.Paths.get(linkPath.toString()),
         java.nio.file.Paths.get(originalPath.toString()));
   }
+
+  /** Returns a per-path lock. The lock is re-entrant. */
+  protected Lock getPathLock(LocalPath path) {
+    return pathLock.get(path);
+  }
 }
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
index a32a4e3..b05c89f 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
@@ -15,12 +15,14 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
 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 java.util.List;
+import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 
 /**
@@ -50,6 +52,8 @@
 
   public static final LocalPath EMPTY = create("");
 
+  private static final Splitter PATH_SPLITTER = Splitter.on('/').omitEmptyStrings();
+
   private final String path;
   private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
   private final OsPathPolicy os;
@@ -228,19 +232,33 @@
    * 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);
+  public List<String> split() {
+    List<String> segments = PATH_SPLITTER.splitToList(path);
+    if (driveStrLength > 1) {
+      return segments.subList(1, segments.size());
     }
     return segments;
   }
 
+  /** Returns the drive of this local path, eg. "/" on Unix or "C:/" on Windows. */
+  public LocalPath getDrive() {
+    if (driveStrLength == 0) {
+      throw new IllegalArgumentException("Cannot get mount of non-absolute path.");
+    }
+    return new LocalPath(path.substring(0, driveStrLength), driveStrLength, os);
+  }
+
+  /**
+   * Returns whether this is the root of the entire file system.
+   *
+   * <p>Please avoid this method. On Unix, this corresponds to the '/' mount point. Windows drives
+   * (C:/) do not have a parent and are not the root of the entire file system, so do not return
+   * true.
+   */
+  public boolean isRoot() {
+    return os.isRoot(this);
+  }
+
   /**
    * Returns whether this path is an ancestor of another path.
    *
@@ -339,10 +357,14 @@
     char getSeparator();
 
     boolean isCaseSensitive();
+
+    boolean isRoot(LocalPath localPath);
   }
 
   @VisibleForTesting
   static class UnixOsPathPolicy implements OsPathPolicy {
+    private static Splitter UNIX_PATH_SPLITTER =
+        Splitter.on(Pattern.compile("/+")).omitEmptyStrings();
 
     @Override
     public int needsToNormalize(String path) {
@@ -362,7 +384,7 @@
         dotCount = c == '.' ? dotCount + 1 : 0;
         prevChar = c;
       }
-      if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
+      if ((n > 1 && prevChar == '/') || dotCount == 1 || dotCount == 2) {
         return NEEDS_NORMALIZE;
       }
       return NORMALIZED;
@@ -377,8 +399,8 @@
         return path;
       }
       boolean isAbsolute = path.charAt(0) == '/';
-      String[] segments = path.split("/+");
-      int segmentCount = removeRelativePaths(segments, isAbsolute ? 1 : 0);
+      String[] segments = Iterables.toArray(UNIX_PATH_SPLITTER.split(path), String.class);
+      int segmentCount = removeRelativePaths(segments, 0);
       StringBuilder sb = new StringBuilder(path.length());
       if (isAbsolute) {
         sb.append('/');
@@ -425,6 +447,11 @@
     public boolean isCaseSensitive() {
       return true;
     }
+
+    @Override
+    public boolean isRoot(LocalPath localPath) {
+      return localPath.path.equals("/");
+    }
   }
 
   /** Mac is a unix file system that is case insensitive. */
@@ -451,8 +478,9 @@
 
     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 static Splitter WINDOWS_PATH_SPLITTER =
+        Splitter.on(Pattern.compile("[\\\\/]+")).omitEmptyStrings();
+
     private final ShortPathResolver shortPathResolver;
 
     interface ShortPathResolver {
@@ -482,10 +510,6 @@
     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
@@ -517,7 +541,7 @@
         dotCount = c == '.' ? dotCount + 1 : 0;
         prevChar = c;
       }
-      if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
+      if ((n > 1 && prevChar == '/') || dotCount == 1 || dotCount == 2) {
         normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
       }
       return normalizationLevel;
@@ -534,26 +558,19 @@
           path = resolvedPath;
         }
       }
-      String[] segments = path.split("[\\\\/]+");
+      String[] segments = Iterables.toArray(WINDOWS_PATH_SPLITTER.splitToList(path), String.class);
       int driveStrLength = getDriveStrLength(path);
       boolean isAbsolute = driveStrLength > 0;
-      int segmentSkipCount = isAbsolute ? 1 : 0;
+      int segmentSkipCount = isAbsolute && driveStrLength > 1 ? 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;
+        char c = path.charAt(0);
+        if (c == '/') {
+          sb.append('/');
         } else {
-          String unixRoot = getUnixRoot();
-          sb.append(unixRoot);
+          sb.append(Character.toUpperCase(c));
+          sb.append(":/");
         }
       }
       int segmentCount = removeRelativePaths(segments, segmentSkipCount);
@@ -570,6 +587,12 @@
     @Override
     public int getDriveStrLength(String path) {
       int n = path.length();
+      if (n == 0) {
+        return 0;
+      }
+      if (path.charAt(0) == '/') {
+        return 1;
+      }
       if (n < 3) {
         return 0;
       }
@@ -622,44 +645,10 @@
       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;
+    @Override
+    public boolean isRoot(LocalPath localPath) {
+      // Return true for Unix paths for testing
+      return localPath.path.equals("/");
     }
   }
 
@@ -685,7 +674,8 @@
   private static int removeRelativePaths(String[] segments, int starti) {
     int segmentCount = 0;
     int shift = starti;
-    for (int i = starti; i < segments.length; ++i) {
+    int n = segments.length;
+    for (int i = starti; i < n; ++i) {
       String segment = segments[i];
       switch (segment) {
         case ".":
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 1d3947d..b09b713 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
@@ -39,7 +39,11 @@
 import java.util.Objects;
 
 /**
- * Instances of this class represent pathnames, forming a tree structure to implement sharing of
+ * NOTE: This class is superseded by {@link LocalPath}. You should prefer storing simple strings /
+ * path fragments, then converting to a {@link LocalPath} only when you need to do local file system
+ * access. A migration is underway.
+ *
+ * <p>Instances of this class represent pathnames, forming a tree structure to implement sharing of
  * common prefixes (parent directory names). A node in these trees is something like foo, bar, ..,
  * ., or /. If the instance is not a root path, it will have a parent path. A path can also have
  * children, which are indexed by name in a map.
@@ -423,6 +427,10 @@
     return builder.toString();
   }
 
+  public LocalPath getLocalPath() {
+    return LocalPath.create(getPathString());
+  }
+
   @Override
   public void repr(SkylarkPrinter printer) {
     printer.append(getPathString());
@@ -500,7 +508,7 @@
    * Path.
    */
   public boolean exists(FileSystem fileSystem) {
-    return fileSystem.exists(this, true);
+    return fileSystem.exists(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #exists(FileSystem, Symlinks)}. */
@@ -519,7 +527,7 @@
    *     link is dereferenced until a file other than a symbolic link is found
    */
   public boolean exists(FileSystem fileSystem, Symlinks followSymlinks) {
-    return fileSystem.exists(this, followSymlinks.toBoolean());
+    return fileSystem.exists(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #getDirectoryEntries(FileSystem)}. */
@@ -540,7 +548,7 @@
    */
   public Collection<Path> getDirectoryEntries(FileSystem fileSystem)
       throws IOException, FileNotFoundException {
-    Collection<String> entries = fileSystem.getDirectoryEntries(this);
+    Collection<String> entries = fileSystem.getDirectoryEntries(this.getLocalPath());
     Collection<Path> result = new ArrayList<>(entries.size());
     for (String entry : entries) {
       result.add(getChild(entry));
@@ -568,7 +576,7 @@
    */
   public Collection<Dirent> readdir(FileSystem fileSystem, Symlinks followSymlinks)
       throws IOException {
-    return fileSystem.readdir(this, followSymlinks.toBoolean());
+    return fileSystem.readdir(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #stat(FileSystem)}. */
@@ -588,7 +596,7 @@
    *     {@code FileStatus} are called.
    */
   public FileStatus stat(FileSystem fileSystem) throws IOException {
-    return fileSystem.stat(this, true);
+    return fileSystem.stat(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #statNullable(FileSystem)}. */
@@ -620,7 +628,7 @@
    * Path.
    */
   public FileStatus statNullable(FileSystem fileSystem, Symlinks symlinks) {
-    return fileSystem.statNullable(this, symlinks.toBoolean());
+    return fileSystem.statNullable(this.getLocalPath(), symlinks.toBoolean());
   }
 
   /** Prefer to use {@link #stat(FileSystem, Symlinks)}. */
@@ -642,7 +650,7 @@
    *     {@code FileStatus} are called
    */
   public FileStatus stat(FileSystem fileSystem, Symlinks followSymlinks) throws IOException {
-    return fileSystem.stat(this, followSymlinks.toBoolean());
+    return fileSystem.stat(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #statIfFound(FileSystem)}. */
@@ -660,7 +668,7 @@
    * Path.
    */
   public FileStatus statIfFound(FileSystem fileSystem) throws IOException {
-    return fileSystem.statIfFound(this, true);
+    return fileSystem.statIfFound(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #statIfFound(FileSystem, Symlinks)}. */
@@ -680,7 +688,7 @@
    *     link is dereferenced until a file other than a symbolic link is found
    */
   public FileStatus statIfFound(FileSystem fileSystem, Symlinks followSymlinks) throws IOException {
-    return fileSystem.statIfFound(this, followSymlinks.toBoolean());
+    return fileSystem.statIfFound(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #isDirectory()} (FileSystem)}. */
@@ -696,7 +704,7 @@
    * Path.
    */
   public boolean isDirectory(FileSystem fileSystem) {
-    return fileSystem.isDirectory(this, true);
+    return fileSystem.isDirectory(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #isDirectory(FileSystem, Symlinks)}. */
@@ -715,7 +723,7 @@
    *     link is dereferenced until a file other than a symbolic link is found
    */
   public boolean isDirectory(FileSystem fileSystem, Symlinks followSymlinks) {
-    return fileSystem.isDirectory(this, followSymlinks.toBoolean());
+    return fileSystem.isDirectory(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #isFile(FileSystem)}. */
@@ -734,7 +742,7 @@
    * it excludes symbolic links and directories.
    */
   public boolean isFile(FileSystem fileSystem) {
-    return fileSystem.isFile(this, true);
+    return fileSystem.isFile(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #isFile(FileSystem, Symlinks)}. */
@@ -756,7 +764,7 @@
    *     link is dereferenced until a file other than a symbolic link is found.
    */
   public boolean isFile(FileSystem fileSystem, Symlinks followSymlinks) {
-    return fileSystem.isFile(this, followSymlinks.toBoolean());
+    return fileSystem.isFile(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #isSpecialFile(FileSystem)}. */
@@ -773,7 +781,7 @@
    * Path.
    */
   public boolean isSpecialFile(FileSystem fileSystem) {
-    return fileSystem.isSpecialFile(this, true);
+    return fileSystem.isSpecialFile(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #isSpecialFile(FileSystem, Symlinks)}. */
@@ -792,7 +800,7 @@
    *     link is dereferenced until a path other than a symbolic link is found.
    */
   public boolean isSpecialFile(FileSystem fileSystem, Symlinks followSymlinks) {
-    return fileSystem.isSpecialFile(this, followSymlinks.toBoolean());
+    return fileSystem.isSpecialFile(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #isSymbolicLink(FileSystem)}. */
@@ -808,7 +816,7 @@
    * Path.
    */
   public boolean isSymbolicLink(FileSystem fileSystem) {
-    return fileSystem.isSymbolicLink(this);
+    return fileSystem.isSymbolicLink(this.getLocalPath());
   }
 
   /**
@@ -1002,7 +1010,7 @@
    */
   public OutputStream getOutputStream(FileSystem fileSystem, boolean append)
       throws IOException, FileNotFoundException {
-    return fileSystem.getOutputStream(this, append);
+    return fileSystem.getOutputStream(this.getLocalPath(), append);
   }
 
   /** Prefer to use {@link #createDirectory(FileSystem)}. */
@@ -1023,7 +1031,7 @@
    * @throws IOException if the directory creation failed for any reason
    */
   public boolean createDirectory(FileSystem fileSystem) throws IOException {
-    return fileSystem.createDirectory(this);
+    return fileSystem.createDirectory(this.getLocalPath());
   }
 
   /** Prefer to use {@link #createSymbolicLink(FileSystem, Path)}. */
@@ -1044,7 +1052,7 @@
    */
   public void createSymbolicLink(FileSystem fileSystem, Path target) throws IOException {
     checkSameFilesystem(target);
-    fileSystem.createSymbolicLink(this, target.asFragment());
+    fileSystem.createSymbolicLink(this.getLocalPath(), target.asFragment().getPathString());
   }
 
   /** Prefer to use {@link #createSymbolicLink(FileSystem, PathFragment)}. */
@@ -1064,7 +1072,7 @@
    * @throws IOException if the creation of the symbolic link was unsuccessful for any reason
    */
   public void createSymbolicLink(FileSystem fileSystem, PathFragment target) throws IOException {
-    fileSystem.createSymbolicLink(this, target);
+    fileSystem.createSymbolicLink(this.getLocalPath(), target.getPathString());
   }
 
   /** Prefer to use {@link #readSymbolicLink(FileSystem)}. */
@@ -1089,7 +1097,7 @@
    *     could not be read for any reason
    */
   public PathFragment readSymbolicLink(FileSystem fileSystem) throws IOException {
-    return fileSystem.readSymbolicLink(this);
+    return PathFragment.create(fileSystem.readSymbolicLink(this.getLocalPath()));
   }
 
   /** Prefer to use {@link #readSymbolicLinkUnchecked(FileSystem)}. */
@@ -1110,7 +1118,7 @@
    *     could not be read for any reason
    */
   public PathFragment readSymbolicLinkUnchecked(FileSystem fileSystem) throws IOException {
-    return fileSystem.readSymbolicLinkUnchecked(this);
+    return PathFragment.create(fileSystem.readSymbolicLinkUnchecked(this.getLocalPath()));
   }
 
   /** Prefer to use {@link #createHardLink(FileSystem, Path)}. */
@@ -1129,7 +1137,7 @@
    * @throws IOException if there was an error executing {@link FileSystem#createHardLink}
    */
   public void createHardLink(FileSystem fileSystem, Path link) throws IOException {
-    fileSystem.createHardLink(link, this);
+    fileSystem.createHardLink(link.getLocalPath(), this.getLocalPath());
   }
 
   /** Prefer to use {@link #resolveSymbolicLinks(FileSystem)}. */
@@ -1150,7 +1158,7 @@
    *     example, the path does not exist)
    */
   public Path resolveSymbolicLinks(FileSystem fileSystem) throws IOException {
-    return fileSystem.resolveSymbolicLinks(this);
+    return fileSystem.getPath(fileSystem.resolveSymbolicLinks(this.getLocalPath()).getPathString());
   }
 
   /** Prefer to use {@link #renameTo(FileSystem, Path)}. */
@@ -1173,7 +1181,7 @@
    */
   public void renameTo(FileSystem fileSystem, Path target) throws IOException {
     checkSameFilesystem(target);
-    fileSystem.renameTo(this, target);
+    fileSystem.renameTo(this.getLocalPath(), target.getLocalPath());
   }
 
   /** Prefer to use {@link #getFileSize(FileSystem)}. */
@@ -1194,7 +1202,7 @@
    * @throws IOException if the file's metadata could not be read, or some other error occurred
    */
   public long getFileSize(FileSystem fileSystem) throws IOException, FileNotFoundException {
-    return fileSystem.getFileSize(this, true);
+    return fileSystem.getFileSize(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #getFileSize(FileSystem, Symlinks)}. */
@@ -1219,7 +1227,7 @@
    */
   public long getFileSize(FileSystem fileSystem, Symlinks followSymlinks)
       throws IOException, FileNotFoundException {
-    return fileSystem.getFileSize(this, followSymlinks.toBoolean());
+    return fileSystem.getFileSize(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #delete(FileSystem)}. */
@@ -1240,7 +1248,7 @@
    * @throws IOException if the deletion failed but the file was present prior to the call
    */
   public boolean delete(FileSystem fileSystem) throws IOException {
-    return fileSystem.delete(this);
+    return fileSystem.delete(this.getLocalPath());
   }
 
   /** Prefer to use {@link #getLastModifiedTime(FileSystem)}. */
@@ -1262,7 +1270,7 @@
    * @throws IOException if the operation failed for any reason
    */
   public long getLastModifiedTime(FileSystem fileSystem) throws IOException {
-    return fileSystem.getLastModifiedTime(this, true);
+    return fileSystem.getLastModifiedTime(this.getLocalPath(), true);
   }
 
   /** Prefer to use {@link #getLastModifiedTime(FileSystem, Symlinks)}. */
@@ -1287,7 +1295,7 @@
    */
   public long getLastModifiedTime(FileSystem fileSystem, Symlinks followSymlinks)
       throws IOException {
-    return fileSystem.getLastModifiedTime(this, followSymlinks.toBoolean());
+    return fileSystem.getLastModifiedTime(this.getLocalPath(), followSymlinks.toBoolean());
   }
 
   /** Prefer to use {@link #setLastModifiedTime(FileSystem, long)}. */
@@ -1312,7 +1320,7 @@
    * @throws IOException if the modification time for the file could not be set for any reason
    */
   public void setLastModifiedTime(FileSystem fileSystem, long newTime) throws IOException {
-    fileSystem.setLastModifiedTime(this, newTime);
+    fileSystem.setLastModifiedTime(this.getLocalPath(), newTime);
   }
 
   /** Prefer to use {@link #getxattr(FileSystem, String)}. */
@@ -1329,7 +1337,7 @@
    * Path.
    */
   public byte[] getxattr(FileSystem fileSystem, String name) throws IOException {
-    return fileSystem.getxattr(this, name);
+    return fileSystem.getxattr(this.getLocalPath(), name);
   }
 
   /** Prefer to use {@link #getFastDigest(FileSystem)}. */
@@ -1346,7 +1354,7 @@
    * Path.
    */
   public byte[] getFastDigest(FileSystem fileSystem) throws IOException {
-    return fileSystem.getFastDigest(this);
+    return fileSystem.getFastDigest(this.getLocalPath());
   }
 
   /** Prefer to use {@link #isValidDigest(FileSystem, byte[])}. */
@@ -1381,7 +1389,7 @@
    * @throws IOException if the digest could not be computed for any reason
    */
   public byte[] getDigest(FileSystem fileSystem) throws IOException {
-    return fileSystem.getDigest(this);
+    return fileSystem.getDigest(this.getLocalPath());
   }
 
   /** Prefer to use {@link #getDigest(FileSystem, HashFunction)}. */
@@ -1401,7 +1409,7 @@
    * @throws IOException if the digest could not be computed for any reason
    */
   public byte[] getDigest(FileSystem fileSystem, HashFunction hashFunction) throws IOException {
-    return fileSystem.getDigest(this, hashFunction);
+    return fileSystem.getDigest(this.getLocalPath(), hashFunction);
   }
 
   /** Prefer to use {@link #getInputStream(FileSystem)}. */
@@ -1420,7 +1428,7 @@
    * @throws IOException if the file was not found or could not be opened for reading
    */
   public InputStream getInputStream(FileSystem fileSystem) throws IOException {
-    return fileSystem.getInputStream(this);
+    return fileSystem.getInputStream(this.getLocalPath());
   }
 
   /**
@@ -1450,7 +1458,7 @@
    *     encountered, or the file's metadata could not be read
    */
   public boolean isWritable(FileSystem fileSystem) throws IOException, FileNotFoundException {
-    return fileSystem.isWritable(this);
+    return fileSystem.isWritable(this.getLocalPath());
   }
 
   /** Prefer to use {@link #setReadable(FileSystem, boolean)}. */
@@ -1472,7 +1480,7 @@
    */
   public void setReadable(FileSystem fileSystem, boolean readable)
       throws IOException, FileNotFoundException {
-    fileSystem.setReadable(this, readable);
+    fileSystem.setReadable(this.getLocalPath(), readable);
   }
 
   /** Prefer to use {@link #setWritable(FileSystem, boolean)}. */
@@ -1496,7 +1504,7 @@
    */
   public void setWritable(FileSystem fileSystem, boolean writable)
       throws IOException, FileNotFoundException {
-    fileSystem.setWritable(this, writable);
+    fileSystem.setWritable(this.getLocalPath(), writable);
   }
 
   /** Prefer to use {@link #isExecutable(FileSystem)}. */
@@ -1517,7 +1525,7 @@
    * @throws IOException if some other I/O error occurred
    */
   public boolean isExecutable(FileSystem fileSystem) throws IOException, FileNotFoundException {
-    return fileSystem.isExecutable(this);
+    return fileSystem.isExecutable(this.getLocalPath());
   }
 
   /** Prefer to use {@link #isReadable(FileSystem)}. */
@@ -1538,7 +1546,7 @@
    * @throws IOException if some other I/O error occurred
    */
   public boolean isReadable(FileSystem fileSystem) throws IOException, FileNotFoundException {
-    return fileSystem.isReadable(this);
+    return fileSystem.isReadable(this.getLocalPath());
   }
 
   /** Prefer to use {@link #setExecutable(FileSystem, boolean)}. */
@@ -1560,7 +1568,7 @@
    */
   public void setExecutable(FileSystem fileSystem, boolean executable)
       throws IOException, FileNotFoundException {
-    fileSystem.setExecutable(this, executable);
+    fileSystem.setExecutable(this.getLocalPath(), executable);
   }
 
   /** Prefer to use {@link #chmod(FileSystem, int)}. */
@@ -1583,7 +1591,7 @@
    * @throws IOException if the metadata change failed, for example because of permissions
    */
   public void chmod(FileSystem fileSystem, int mode) throws IOException {
-    fileSystem.chmod(this, mode);
+    fileSystem.chmod(this.getLocalPath(), mode);
   }
 
   /** Prefer to use {@link #prefetchPackageAsync(FileSystem, int)}. */
@@ -1593,7 +1601,7 @@
   }
 
   public void prefetchPackageAsync(FileSystem fileSystem, int maxDirs) {
-    fileSystem.prefetchPackageAsync(this, maxDirs);
+    fileSystem.prefetchPackageAsync(this.getLocalPath(), maxDirs);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java b/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java
deleted file mode 100644
index fd783c5..0000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathTrie.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import com.google.common.base.Preconditions;
-import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A trie that operates on path segments.
- *
- * @param <T> the type of the values.
- */
-@ThreadCompatible
-public class PathTrie<T> {
-  @SuppressWarnings("unchecked")
-  private static class Node<T> {
-    private Node() {
-      children = new HashMap<>();
-    }
-
-    private T value;
-    private Map<String, Node<T>> children;
-  }
-
-  private final Node<T> root;
-
-  public PathTrie() {
-    root = new Node<T>();
-  }
-
-  /**
-   * Puts a value in the trie.
-   *
-   * @param key must be an absolute path.
-   */
-  public void put(PathFragment key, T value) {
-    Preconditions.checkArgument(key.isAbsolute(), "PathTrie only accepts absolute paths as keys.");
-    Node<T> current = root;
-    for (String segment : key.getSegments()) {
-      current.children.putIfAbsent(segment, new Node<T>());
-      current = current.children.get(segment);
-    }
-    current.value = value;
-  }
-
-  /**
-   * Gets a value from the trie. If there is an entry with the same key, that will be returned,
-   * otherwise, the value corresponding to the key that matches the longest prefix of the input.
-   */
-  public T get(PathFragment key) {
-    Node<T> current = root;
-    T lastValue = current.value;
-
-    for (String segment : key.getSegments()) {
-      if (current.children.containsKey(segment)) {
-        current = current.children.get(segment);
-        // Track the values of increasing matching prefixes.
-        if (current.value != null) {
-          lastValue = current.value;
-        }
-      } else {
-        // We've reached the longest prefix, no further to go.
-        break;
-      }
-    }
-
-    return lastValue;
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
index bfcc4f9..0eab17c 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystem.java
@@ -17,20 +17,21 @@
 import java.io.OutputStream;
 
 /**
- * An abstract partial implementation of FileSystem for read-only
- * implementations.
+ * An abstract partial implementation of FileSystem for read-only implementations.
  *
  * <p>Any ReadonlyFileSystem does not support the following:
+ *
  * <ul>
- * <li>{@link #createDirectory(Path)}</li>
- * <li>{@link #createSymbolicLink(Path, PathFragment)}</li>
- * <li>{@link #delete(Path)}</li>
- * <li>{@link #getOutputStream(Path)}</li>
- * <li>{@link #renameTo(Path, Path)}</li>
- * <li>{@link #setExecutable(Path, boolean)}</li>
- * <li>{@link #setLastModifiedTime(Path, long)}</li>
- * <li>{@link #setWritable(Path, boolean)}</li>
+ *   <li>{@link #createDirectory(LocalPath)}
+ *   <li>{@link #createSymbolicLink(LocalPath, String)}
+ *   <li>{@link #delete(LocalPath)}
+ *   <li>{@link #getOutputStream(LocalPath)}
+ *   <li>{@link #renameTo(LocalPath, LocalPath)}
+ *   <li>{@link #setExecutable(LocalPath, boolean)}
+ *   <li>{@link #setLastModifiedTime(LocalPath, long)}
+ *   <li>{@link #setWritable(LocalPath, boolean)}
  * </ul>
+ *
  * The above calls will always result in an {@link IOException}.
  */
 public abstract class ReadonlyFileSystem extends AbstractFileSystem {
@@ -46,37 +47,37 @@
   }
 
   @Override
-  protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+  protected OutputStream getOutputStream(LocalPath path, boolean append) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable) {
+  protected void setExecutable(LocalPath path, boolean executable) {
     throw new UnsupportedOperationException("setExecutable");
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     return false;
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return false;
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     return false;
   }
 
@@ -86,32 +87,32 @@
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
+  public boolean createDirectory(LocalPath path) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath) throws IOException {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
+  public boolean delete(LocalPath path) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException {
     throw modificationException();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystemWithCustomStat.java b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystemWithCustomStat.java
index de5daca..46e058c 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystemWithCustomStat.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ReadonlyFileSystemWithCustomStat.java
@@ -31,37 +31,37 @@
   }
 
   @Override
-  protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+  protected OutputStream getOutputStream(LocalPath path, boolean append) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable) {
+  protected void setExecutable(LocalPath path, boolean executable) {
     throw new UnsupportedOperationException("setExecutable");
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     return false;
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return false;
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     return false;
   }
 
@@ -71,33 +71,33 @@
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
+  public boolean createDirectory(LocalPath path) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
     throw modificationException();
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath) throws IOException {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
+  public boolean delete(LocalPath path) throws IOException {
     throw modificationException();
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
     throw modificationException();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
index 824e71d..504aa937 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnionFileSystem.java
@@ -15,12 +15,14 @@
 package com.google.devtools.build.lib.vfs;
 
 import com.google.common.base.Preconditions;
-import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.concurrent.ThreadSafety;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 
@@ -42,12 +44,21 @@
  * are not currently supported.
  */
 @ThreadSafety.ThreadSafe
-public class UnionFileSystem extends FileSystem {
+public final class UnionFileSystem extends FileSystem {
 
-  // Prefix trie index, allowing children to easily inherit prefix mappings
-  // of their parents.
-  // This does not currently handle unicode filenames.
-  private final PathTrie<FileSystem> pathDelegate;
+  private static class FileSystemAndPrefix {
+    final LocalPath prefix;
+    final FileSystem fileSystem;
+
+    public FileSystemAndPrefix(LocalPath prefix, FileSystem fileSystem) {
+      this.prefix = prefix;
+      this.fileSystem = fileSystem;
+    }
+  }
+
+  // List of file systems and their mappings, sorted by prefix length descending.
+  private final List<FileSystemAndPrefix> fileSystems;
+  private final FileSystem rootFileSystem;
 
   // True if the file path is case-sensitive on all the FileSystem
   // or False if they are all case-insensitive, otherwise error.
@@ -59,30 +70,35 @@
    * @param prefixMapping map of path prefixes to {@link FileSystem}s
    * @param rootFileSystem root for default requests; i.e. mapping of "/"
    */
-  public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) {
+  public UnionFileSystem(Map<LocalPath, FileSystem> prefixMapping, FileSystem rootFileSystem) {
     super();
     Preconditions.checkNotNull(prefixMapping);
     Preconditions.checkNotNull(rootFileSystem);
     Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
     Preconditions.checkArgument(
-        !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
+        !prefixMapping.containsKey(LocalPath.EMPTY),
         "Attempted to specify an explicit root prefix mapping; "
             + "please use the rootFileSystem argument instead.");
 
-    this.pathDelegate = new PathTrie<>();
+    this.fileSystems = new ArrayList<>();
+    this.rootFileSystem = rootFileSystem;
     this.isCaseSensitive = rootFileSystem.isFilePathCaseSensitive();
 
-    for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
+    for (Map.Entry<LocalPath, FileSystem> prefix : prefixMapping.entrySet()) {
       FileSystem delegate = prefix.getValue();
       Preconditions.checkArgument(
           delegate.isFilePathCaseSensitive() == this.isCaseSensitive,
           "The case sensitiveness of FileSystem are different in UnionFileSystem");
-      PathFragment prefixPath = prefix.getKey();
+      LocalPath prefixPath = prefix.getKey();
 
       // Extra slash prevents within-directory mappings, which Path can't handle.
-      pathDelegate.put(prefixPath, delegate);
+      fileSystems.add(new FileSystemAndPrefix(prefixPath, delegate));
     }
-    pathDelegate.put(PathFragment.ROOT_FRAGMENT, rootFileSystem);
+    // Order by length descending. This ensures that more specific mapping takes precedence
+    // when we try to find the file system of a given path.
+    Comparator<FileSystemAndPrefix> comparator =
+        Comparator.comparing(f -> f.prefix.getPathString().length());
+    fileSystems.sort(comparator.reversed());
   }
 
   /**
@@ -92,19 +108,24 @@
    * @param path the {@link Path} to map to a filesystem
    * @throws IllegalArgumentException if no delegate exists for the path
    */
-  protected FileSystem getDelegate(Path path) {
+  FileSystem getDelegate(LocalPath path) {
     Preconditions.checkNotNull(path);
-    FileSystem immediateDelegate = pathDelegate.get(path.asFragment());
-
-    // Should never actually happen if the root delegate is present.
-    Preconditions.checkNotNull(immediateDelegate, "No delegate filesystem exists for %s", path);
-    return immediateDelegate;
+    FileSystem delegate = null;
+    // Linearly iterate over each mapped file system and find the one that handles this path.
+    // For small number of mappings, this will be more efficient than using a trie
+    for (FileSystemAndPrefix fileSystemAndPrefix : this.fileSystems) {
+      if (path.startsWith(fileSystemAndPrefix.prefix)) {
+        delegate = fileSystemAndPrefix.fileSystem;
+        break;
+      }
+    }
+    return delegate != null ? delegate : rootFileSystem;
   }
 
   // Associates the path with the root of the given delegate filesystem.
   // Necessary to avoid null pointer problems inside of the delegates.
-  protected Path adjustPath(Path path, FileSystem delegate) {
-    return delegate.getPath(path.asFragment());
+  LocalPath adjustPath(LocalPath path, FileSystem delegate) {
+    return path;
   }
 
   /**
@@ -114,20 +135,20 @@
    * @param path {@link Path} to the symbolic link
    */
   @Override
-  protected PathFragment readSymbolicLink(Path path) throws IOException {
+  protected String readSymbolicLink(LocalPath path) throws IOException {
     Preconditions.checkNotNull(path);
     FileSystem delegate = getDelegate(path);
     return delegate.readSymbolicLink(adjustPath(path, delegate));
   }
 
   @Override
-  protected PathFragment resolveOneLink(Path path) throws IOException {
+  protected String resolveOneLink(LocalPath path) throws IOException {
     Preconditions.checkNotNull(path);
     FileSystem delegate = getDelegate(path);
     return delegate.resolveOneLink(adjustPath(path, delegate));
   }
 
-  private void checkModifiable(Path path) {
+  private void checkModifiable(LocalPath path) {
     if (!supportsModifications(path)) {
       throw new UnsupportedOperationException(
           String.format("Modifications to this %s are disabled.", getClass().getSimpleName()));
@@ -135,21 +156,21 @@
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     FileSystem delegate = getDelegate(path);
     path = adjustPath(path, delegate);
     return delegate.supportsModifications(path);
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     FileSystem delegate = getDelegate(path);
     path = adjustPath(path, delegate);
     return delegate.supportsSymbolicLinksNatively(path);
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     FileSystem delegate = getDelegate(path);
     path = adjustPath(path, delegate);
     return delegate.supportsHardLinksNatively(path);
@@ -161,7 +182,7 @@
   }
 
   @Override
-  public String getFileSystemType(Path path) {
+  public String getFileSystemType(LocalPath path) {
     try {
       path = internalResolveSymlink(path);
     } catch (IOException e) {
@@ -172,14 +193,14 @@
   }
 
   @Override
-  protected byte[] getDigest(Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.getDigest(adjustPath(path, delegate), hashFunction);
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
+  public boolean createDirectory(LocalPath path) throws IOException {
     checkModifiable(path);
     // When creating the exact directory that is mapped,
     // create it on both the parent's delegate and the path's delegate.
@@ -192,7 +213,7 @@
     //   ls / ("foo" would be missing if not created on the parent)
     //   ls /foo (would fail if foo weren't also present on the child)
     FileSystem delegate = getDelegate(path);
-    Path parent = path.getParentDirectory();
+    LocalPath parent = path.getParentDirectory();
     if (parent != null) {
       parent = internalResolveSymlink(parent);
       FileSystem parentDelegate = getDelegate(parent);
@@ -206,28 +227,28 @@
   }
 
   @Override
-  protected long getFileSize(Path path, boolean followSymlinks) throws IOException {
+  protected long getFileSize(LocalPath path, boolean followSymlinks) throws IOException {
     path = followSymlinks ? internalResolveSymlink(path) : path;
     FileSystem delegate = getDelegate(path);
     return delegate.getFileSize(adjustPath(path, delegate), false);
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
+  public boolean delete(LocalPath path) throws IOException {
     checkModifiable(path);
     FileSystem delegate = getDelegate(path);
     return delegate.delete(adjustPath(path, delegate));
   }
 
   @Override
-  protected long getLastModifiedTime(Path path, boolean followSymlinks) throws IOException {
+  protected long getLastModifiedTime(LocalPath path, boolean followSymlinks) throws IOException {
     path = followSymlinks ? internalResolveSymlink(path) : path;
     FileSystem delegate = getDelegate(path);
     return delegate.getLastModifiedTime(adjustPath(path, delegate), false);
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
     path = internalResolveSymlink(path);
     checkModifiable(path);
     FileSystem delegate = getDelegate(path);
@@ -235,14 +256,14 @@
   }
 
   @Override
-  protected boolean isSymbolicLink(Path path) {
+  protected boolean isSymbolicLink(LocalPath path) {
     FileSystem delegate = getDelegate(path);
     path = adjustPath(path, delegate);
     return delegate.isSymbolicLink(path);
   }
 
   @Override
-  protected boolean isDirectory(Path path, boolean followSymlinks) {
+  protected boolean isDirectory(LocalPath path, boolean followSymlinks) {
     try {
       path = followSymlinks ? internalResolveSymlink(path) : path;
     } catch (IOException e) {
@@ -253,7 +274,7 @@
   }
 
   @Override
-  protected boolean isFile(Path path, boolean followSymlinks) {
+  protected boolean isFile(LocalPath path, boolean followSymlinks) {
     try {
       path = followSymlinks ? internalResolveSymlink(path) : path;
     } catch (IOException e) {
@@ -264,7 +285,7 @@
   }
 
   @Override
-  protected boolean isSpecialFile(Path path, boolean followSymlinks) {
+  protected boolean isSpecialFile(LocalPath path, boolean followSymlinks) {
     try {
       path = followSymlinks ? internalResolveSymlink(path) : path;
     } catch (IOException e) {
@@ -275,7 +296,7 @@
   }
 
   @Override
-  protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) throws IOException {
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
     checkModifiable(linkPath);
     if (!supportsSymbolicLinksNatively(linkPath)) {
       throw new UnsupportedOperationException(
@@ -287,7 +308,7 @@
   }
 
   @Override
-  protected boolean exists(Path path, boolean followSymlinks) {
+  protected boolean exists(LocalPath path, boolean followSymlinks) {
     try {
       path = followSymlinks ? internalResolveSymlink(path) : path;
     } catch (IOException e) {
@@ -298,7 +319,7 @@
   }
 
   @Override
-  protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException {
     path = followSymlinks ? internalResolveSymlink(path) : path;
     FileSystem delegate = getDelegate(path);
     return delegate.stat(adjustPath(path, delegate), false);
@@ -308,7 +329,7 @@
   // UnixFileSystem implements statNullable and stat as separate codepaths.
   // More generally, we wish to delegate all filesystem operations.
   @Override
-  protected FileStatus statNullable(Path path, boolean followSymlinks) {
+  protected FileStatus statNullable(LocalPath path, boolean followSymlinks) {
     try {
       path = followSymlinks ? internalResolveSymlink(path) : path;
     } catch (IOException e) {
@@ -320,7 +341,7 @@
 
   @Override
   @Nullable
-  protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus statIfFound(LocalPath path, boolean followSymlinks) throws IOException {
     path = followSymlinks ? internalResolveSymlink(path) : path;
     FileSystem delegate = getDelegate(path);
     return delegate.statIfFound(adjustPath(path, delegate), false);
@@ -333,35 +354,30 @@
    * @param path the {@link Path} whose children are to be retrieved
    */
   @Override
-  protected Collection<String> getDirectoryEntries(Path path) throws IOException {
+  protected Collection<String> getDirectoryEntries(LocalPath path) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
-    Path resolvedPath = adjustPath(path, delegate);
-    Collection<Path> entries = resolvedPath.getDirectoryEntries();
-    Collection<String> result = Lists.newArrayListWithCapacity(entries.size());
-    for (Path entry : entries) {
-      result.add(entry.getBaseName());
-    }
-    return result;
+    LocalPath resolvedPath = adjustPath(path, delegate);
+    return delegate.getDirectoryEntries(resolvedPath);
   }
 
   // No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
   @Override
-  protected Collection<Dirent> readdir(Path path, boolean followSymlinks) throws IOException {
+  protected Collection<Dirent> readdir(LocalPath path, boolean followSymlinks) throws IOException {
     path = followSymlinks ? internalResolveSymlink(path) : path;
     FileSystem delegate = getDelegate(path);
     return delegate.readdir(adjustPath(path, delegate), false);
   }
 
   @Override
-  protected boolean isReadable(Path path) throws IOException {
+  protected boolean isReadable(LocalPath path) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.isReadable(adjustPath(path, delegate));
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     path = internalResolveSymlink(path);
     checkModifiable(path);
     FileSystem delegate = getDelegate(path);
@@ -369,7 +385,7 @@
   }
 
   @Override
-  protected boolean isWritable(Path path) throws IOException {
+  protected boolean isWritable(LocalPath path) throws IOException {
     if (!supportsModifications(path)) {
       return false;
     }
@@ -379,7 +395,7 @@
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     checkModifiable(path);
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
@@ -387,14 +403,14 @@
   }
 
   @Override
-  protected boolean isExecutable(Path path) throws IOException {
+  protected boolean isExecutable(LocalPath path) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.isExecutable(adjustPath(path, delegate));
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable) throws IOException {
+  protected void setExecutable(LocalPath path, boolean executable) throws IOException {
     path = internalResolveSymlink(path);
     checkModifiable(path);
     FileSystem delegate = getDelegate(path);
@@ -402,28 +418,28 @@
   }
 
   @Override
-  protected byte[] getFastDigest(Path path, HashFunction hashFunction) throws IOException {
+  protected byte[] getFastDigest(LocalPath path, HashFunction hashFunction) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.getFastDigest(adjustPath(path, delegate), hashFunction);
   }
 
   @Override
-  public byte[] getxattr(Path path, String name) throws IOException {
+  public byte[] getxattr(LocalPath path, String name) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.getxattr(adjustPath(path, delegate), name);
   }
 
   @Override
-  protected InputStream getInputStream(Path path) throws IOException {
+  protected InputStream getInputStream(LocalPath path) throws IOException {
     path = internalResolveSymlink(path);
     FileSystem delegate = getDelegate(path);
     return delegate.getInputStream(adjustPath(path, delegate));
   }
 
   @Override
-  protected OutputStream getOutputStream(Path path, boolean append) throws IOException {
+  protected OutputStream getOutputStream(LocalPath path, boolean append) throws IOException {
     path = internalResolveSymlink(path);
     checkModifiable(path);
     FileSystem delegate = getDelegate(path);
@@ -431,7 +447,7 @@
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath) throws IOException {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
     sourcePath = internalResolveSymlink(sourcePath);
     FileSystem sourceDelegate = getDelegate(sourcePath);
     if (!sourceDelegate.supportsModifications(sourcePath)) {
@@ -458,13 +474,14 @@
     } else {
       // Copy across filesystems, then delete.
       // copyFile throws on failure, so delete will never be reached if it fails.
-      FileSystemUtils.copyFile(sourcePath, targetPath);
+      FileSystemUtils.copyFile(sourceDelegate, sourcePath, targetDelegate, targetPath);
       sourceDelegate.delete(sourcePath);
     }
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath) throws IOException {
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
+      throws IOException {
     checkModifiable(linkPath);
 
     originalPath = internalResolveSymlink(originalPath);
@@ -480,9 +497,9 @@
         adjustPath(linkPath, linkDelegate), adjustPath(originalPath, originalDelegate));
   }
 
-  private Path internalResolveSymlink(Path path) throws IOException {
+  private LocalPath internalResolveSymlink(LocalPath path) throws IOException {
     while (isSymbolicLink(path)) {
-      PathFragment pathFragment = resolveOneLink(path);
+      String pathFragment = resolveOneLink(path);
       path = path.getRelative(pathFragment);
     }
     return path;
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
index 93c4eb2..333c3bb 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
@@ -17,7 +17,7 @@
 import com.google.devtools.build.lib.clock.Clock;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.vfs.FileStatus;
-import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.LocalPath;
 import java.io.IOException;
 
 /**
@@ -201,6 +201,5 @@
    * @param targetPath where the inode is relocated.
    * @throws IOException
    */
-  protected void movedTo(Path targetPath) throws IOException {
-  }
+  protected void movedTo(LocalPath targetPath) throws IOException {}
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
index ff6d88a..0d35942 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.vfs.FileAccessException;
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.LocalPath;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.ByteArrayInputStream;
@@ -48,7 +49,8 @@
 @ThreadSafe
 public class InMemoryFileSystem extends FileSystem {
 
-  private final PathFragment scopeRoot;
+  private final LocalPath scopeRoot;
+  private final int scopeSegmentCount;
   private final Clock clock;
 
   // The root inode (a directory).
@@ -82,6 +84,7 @@
     this.clock = clock;
     this.rootInode = newRootInode(clock);
     this.scopeRoot = null;
+    this.scopeSegmentCount = 0;
   }
 
   /**
@@ -89,7 +92,8 @@
    * not below scopeRoot is considered to be out of scope.
    */
   public InMemoryFileSystem(Clock clock, PathFragment scopeRoot) {
-    this.scopeRoot = scopeRoot;
+    this.scopeRoot = scopeRoot != null ? LocalPath.create(scopeRoot.getPathString()) : null;
+    this.scopeSegmentCount = scopeRoot != null ? scopeRoot.segmentCount() : 0;
     this.clock = clock;
     this.rootInode = newRootInode(clock);
   }
@@ -109,7 +113,7 @@
    * @param normalizedPath input path, expected to be normalized such that all ".." and "." segments
    *     are removed (with the exception of a possible prefix sequence of contiguous ".." segments)
    */
-  private boolean inScope(int parentDepth, PathFragment normalizedPath) {
+  private boolean inScope(int parentDepth, LocalPath normalizedPath) {
     if (scopeRoot == null) {
       return true;
     } else if (normalizedPath.isAbsolute()) {
@@ -120,7 +124,7 @@
       // unnecessary re-delegation back into the same FS. we're choosing to forgo that
       // optimization under the assumption that such scenarios are rare and unimportant to
       // overall performance. We can always enhance this if needed.
-      return parentDepth - leadingParentReferences(normalizedPath) >= scopeRoot.segmentCount();
+      return parentDepth - leadingParentReferences(normalizedPath) >= scopeSegmentCount;
     }
   }
 
@@ -131,12 +135,14 @@
    * <p>Example allowed inputs: "/absolute/path", "relative/path", "../../relative/path". Example
    * disallowed inputs: "/absolute/path/../path2", "relative/../path", "../relative/../p".
    */
-  private int leadingParentReferences(PathFragment normalizedPath) {
+  private int leadingParentReferences(LocalPath normalizedPath) {
     int leadingParentReferences = 0;
-    for (int i = 0;
-        i < normalizedPath.segmentCount() && normalizedPath.getSegment(i).equals("..");
-        i++) {
-      leadingParentReferences++;
+    for (String segment : normalizedPath.split()) {
+      if (segment.equals("..")) {
+        ++leadingParentReferences;
+      } else {
+        break;
+      }
     }
     return leadingParentReferences;
   }
@@ -245,11 +251,10 @@
     }
 
     /**
-     * Returns a new IOException for the error. The exception message
-     * contains 'path', and is consistent with the messages returned by
-     * c.g.common.unix.FilesystemUtils.
+     * Returns a new IOException for the error. The exception message contains 'path', and is
+     * consistent with the messages returned by c.g.common.unix.FilesystemUtils.
      */
-    public IOException exception(Path path) throws IOException {
+    public IOException exception(LocalPath path) throws IOException {
       String m = path + " (" + message + ")";
       if (this == EACCES) {
         throw new FileAccessExceptionWithError(m, this);
@@ -267,48 +272,44 @@
    * <p>If <code>/proc/mounts</code> does not exist return {@code "inmemoryfs"}.
    */
   @Override
-  public String getFileSystemType(Path path) {
-    return path.getRelative("/proc/mounts").exists() ? super.getFileSystemType(path) : "inmemoryfs";
+  public String getFileSystemType(LocalPath path) {
+    return exists(path.getRelative("/proc/mounts")) ? super.getFileSystemType(path) : "inmemoryfs";
   }
 
-  /****************************************************************************
-   * "Kernel" primitives: basic directory lookup primitives, in topological
-   * order.
+  /**
+   * ************************************************************************** "Kernel" primitives:
+   * basic directory lookup primitives, in topological order.
    */
 
   /**
-   * Unlinks the entry 'child' from its existing parent directory 'dir'. Dual to
-   * insert. This succeeds even if 'child' names a non-empty directory; we need
-   * that for renameTo. 'child' must be a member of its parent directory,
-   * however. Fails if the directory was read-only.
+   * Unlinks the entry 'child' from its existing parent directory 'dir'. Dual to insert. This
+   * succeeds even if 'child' names a non-empty directory; we need that for renameTo. 'child' must
+   * be a member of its parent directory, however. Fails if the directory was read-only.
    */
-  private void unlink(InMemoryDirectoryInfo dir, String child, Path errorPath)
+  private void unlink(InMemoryDirectoryInfo dir, String child, LocalPath errorPath)
       throws IOException {
     if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
     dir.removeChild(child);
   }
 
   /**
-   * Inserts inode 'childInode' into the existing directory 'dir' under the
-   * specified 'name'.  Dual to unlink.  Fails if the directory was read-only.
+   * Inserts inode 'childInode' into the existing directory 'dir' under the specified 'name'. Dual
+   * to unlink. Fails if the directory was read-only.
    */
-  private void insert(InMemoryDirectoryInfo dir, String child,
-                      InMemoryContentInfo childInode, Path errorPath)
+  private void insert(
+      InMemoryDirectoryInfo dir, String child, InMemoryContentInfo childInode, LocalPath errorPath)
       throws IOException {
     if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
     dir.addChild(child, childInode);
   }
 
   /**
-   * Given an existing directory 'dir', looks up 'name' within it and returns
-   * its inode. Assumes the file exists, unless 'create', in which case it will
-   * try to create it. May fail with ENOTDIR, EACCES, ENOENT. Error messages
-   * will be reported against file 'path'.
+   * Given an existing directory 'dir', looks up 'name' within it and returns its inode. Assumes the
+   * file exists, unless 'create', in which case it will try to create it. May fail with ENOTDIR,
+   * EACCES, ENOENT. Error messages will be reported against file 'path'.
    */
-  private InMemoryContentInfo directoryLookup(InMemoryContentInfo dir,
-                                              String name,
-                                              boolean create,
-                                              Path path) throws IOException {
+  private InMemoryContentInfo directoryLookup(
+      InMemoryContentInfo dir, String name, boolean create, LocalPath path) throws IOException {
     if (!dir.isDirectory()) { throw Error.ENOTDIR.exception(path); }
     InMemoryDirectoryInfo imdi = (InMemoryDirectoryInfo) dir;
     if (!imdi.isExecutable()) { throw Error.EACCES.exception(path); }
@@ -336,7 +337,8 @@
    *
    * <p>May fail with ENOTDIR, ENOENT, EACCES, ELOOP.
    */
-  private synchronized InMemoryContentInfo pathWalk(Path path, boolean create) throws IOException {
+  private synchronized InMemoryContentInfo pathWalk(LocalPath path, boolean create)
+      throws IOException {
     // Implementation note: This is where we check for out-of-scope symlinks and
     // trigger re-delegation to another file system accordingly. This code handles
     // both absolute and relative symlinks. Some assumptions we make: First, only
@@ -348,8 +350,7 @@
     // and it may only appear as part of a contiguous prefix sequence.
 
     Stack<String> stack = new Stack<>();
-    PathFragment rootPathFragment = getRootDirectory().asFragment();
-    for (Path p = path; !p.asFragment().equals(rootPathFragment); p = p.getParentDirectory()) {
+    for (LocalPath p = path; p != null && !p.isRoot(); p = p.getParentDirectory()) {
       stack.push(p.getBaseName());
     }
 
@@ -366,7 +367,7 @@
       // ENOENT on last segment with 'create' => create a new file.
       InMemoryContentInfo child = directoryLookup(inode, name, create && stack.isEmpty(), path);
       if (child.isSymbolicLink()) {
-        PathFragment linkTarget = ((InMemoryLinkInfo) child).getNormalizedLinkContent();
+        LocalPath linkTarget = ((InMemoryLinkInfo) child).getNormalizedLinkContent();
         if (!inScope(parentDepth, linkTarget)) {
           throw Error.ENOENT.exception(path);
         }
@@ -377,8 +378,9 @@
         if (traversals > MAX_TRAVERSALS) {
           throw Error.ELOOP.exception(path);
         }
-        for (int ii = linkTarget.segmentCount() - 1; ii >= 0; --ii) {
-          stack.push(linkTarget.getSegment(ii)); // Note this may include ".." segments.
+        List<String> linkSegments = linkTarget.split();
+        for (int ii = linkSegments.size() - 1; ii >= 0; --ii) {
+          stack.push(linkSegments.get(ii)); // Note this may include ".." segments.
         }
       } else {
         inode = child;
@@ -388,12 +390,11 @@
   }
 
   /**
-   * Given 'path', returns the existing directory inode it designates,
-   * following symbolic links.
+   * Given 'path', returns the existing directory inode it designates, following symbolic links.
    *
    * <p>May fail with ENOTDIR, or any exception from pathWalk.
    */
-  private InMemoryDirectoryInfo getDirectory(Path path) throws IOException {
+  private InMemoryDirectoryInfo getDirectory(LocalPath path) throws IOException {
     InMemoryContentInfo dirInfo = pathWalk(path, false);
     if (!dirInfo.isDirectory()) {
       throw Error.ENOTDIR.exception(path);
@@ -403,28 +404,28 @@
   }
 
   /**
-   * Helper method for stat, scopeLimitedStat: lock the internal state and return the
-   * path's (no symlink-followed) stat if the path's parent directory is within scope,
-   * else return an "out of scope" reference to the path's parent directory (which will
-   * presumably be re-delegated to another FS).
+   * Helper method for stat, scopeLimitedStat: lock the internal state and return the path's (no
+   * symlink-followed) stat if the path's parent directory is within scope, else return an "out of
+   * scope" reference to the path's parent directory (which will presumably be re-delegated to
+   * another FS).
    */
-  private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path)
-      throws IOException  {
+  private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(LocalPath path)
+      throws IOException {
     InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory());
     return directoryLookup(dirInfo, path.getBaseName(), /*create=*/ false, path);
   }
 
   /**
-   * Given 'path', returns the existing inode it designates, optionally
-   * following symbolic links.  Analogous to UNIX stat(2)/lstat(2), except that
-   * it returns a mutable inode we can modify directly.
+   * Given 'path', returns the existing inode it designates, optionally following symbolic links.
+   * Analogous to UNIX stat(2)/lstat(2), except that it returns a mutable inode we can modify
+   * directly.
    */
   @Override
-  public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+  public FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException {
     if (followSymlinks) {
       return scopeLimitedStat(path, true);
     } else {
-      if (path.equals(getRootDirectory())) {
+      if (path.isRoot()) {
         return rootInode;
       } else {
         return getNoFollowStatOrOutOfScopeParent(path);
@@ -434,7 +435,7 @@
 
   @Override
   @Nullable
-  public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
+  public FileStatus statIfFound(LocalPath path, boolean followSymlinks) throws IOException {
     try {
       return stat(path, followSymlinks);
     } catch (IOException e) {
@@ -452,12 +453,12 @@
    * Version of stat that returns an inode if the input path stays entirely within this file
    * system's scope, otherwise throws.
    */
-  private InMemoryContentInfo scopeLimitedStat(Path path, boolean followSymlinks)
+  private InMemoryContentInfo scopeLimitedStat(LocalPath path, boolean followSymlinks)
       throws IOException {
     if (followSymlinks) {
       return pathWalk(path, false);
     } else {
-      if (path.equals(getRootDirectory())) {
+      if (path.isRoot()) {
         return rootInode;
       } else {
         return getNoFollowStatOrOutOfScopeParent(path);
@@ -465,21 +466,20 @@
     }
   }
 
-  /****************************************************************************
-   *  FileSystem methods
+  /**
+   * ************************************************************************** FileSystem methods
    */
 
   /**
-   * This is a helper routing for {@link #resolveSymbolicLinks(Path)}, i.e.
-   * the "user-mode" routing for canonicalising paths. It is analogous to the
-   * code in glibc's realpath(3).
+   * This is a helper routing for {@link #resolveSymbolicLinks(LocalPath)}, i.e. the "user-mode"
+   * routing for canonicalising paths. It is analogous to the code in glibc's realpath(3).
    *
-   * <p>Just like realpath, resolveSymbolicLinks requires a quadratic number of
-   * directory lookups: n path segments are statted, and each stat requires a
-   * linear amount of work in the "kernel" routine.
+   * <p>Just like realpath, resolveSymbolicLinks requires a quadratic number of directory lookups: n
+   * path segments are statted, and each stat requires a linear amount of work in the "kernel"
+   * routine.
    */
   @Override
-  protected PathFragment resolveOneLink(Path path) throws IOException {
+  protected String resolveOneLink(LocalPath path) throws IOException {
     // Beware, this seemingly simple code belies the complex specification of
     // FileSystem.resolveOneLink().
     InMemoryContentInfo status = scopeLimitedStat(path, false);
@@ -487,7 +487,7 @@
   }
 
   @Override
-  protected boolean isDirectory(Path path, boolean followSymlinks) {
+  protected boolean isDirectory(LocalPath path, boolean followSymlinks) {
     try {
       return stat(path, followSymlinks).isDirectory();
     } catch (IOException e) {
@@ -496,7 +496,7 @@
   }
 
   @Override
-  protected boolean isFile(Path path, boolean followSymlinks) {
+  protected boolean isFile(LocalPath path, boolean followSymlinks) {
     try {
       return stat(path, followSymlinks).isFile();
     } catch (IOException e) {
@@ -505,7 +505,7 @@
   }
 
   @Override
-  protected boolean isSpecialFile(Path path, boolean followSymlinks) {
+  protected boolean isSpecialFile(LocalPath path, boolean followSymlinks) {
     try {
       return stat(path, followSymlinks).isSpecialFile();
     } catch (IOException e) {
@@ -514,7 +514,7 @@
   }
 
   @Override
-  protected boolean isSymbolicLink(Path path) {
+  protected boolean isSymbolicLink(LocalPath path) {
     try {
       return stat(path, false).isSymbolicLink();
     } catch (IOException e) {
@@ -523,7 +523,7 @@
   }
 
   @Override
-  protected boolean exists(Path path, boolean followSymlinks) {
+  protected boolean exists(LocalPath path, boolean followSymlinks) {
     try {
       stat(path, followSymlinks);
       return true;
@@ -533,13 +533,13 @@
   }
 
   @Override
-  protected boolean isReadable(Path path) throws IOException {
+  protected boolean isReadable(LocalPath path) throws IOException {
     InMemoryContentInfo status = scopeLimitedStat(path, true);
     return status.isReadable();
   }
 
   @Override
-  protected void setReadable(Path path, boolean readable) throws IOException {
+  protected void setReadable(LocalPath path, boolean readable) throws IOException {
     synchronized (this) {
       InMemoryContentInfo status = scopeLimitedStat(path, true);
       status.setReadable(readable);
@@ -547,13 +547,13 @@
   }
 
   @Override
-  protected boolean isWritable(Path path) throws IOException {
+  protected boolean isWritable(LocalPath path) throws IOException {
     InMemoryContentInfo status = scopeLimitedStat(path, true);
     return status.isWritable();
   }
 
   @Override
-  public void setWritable(Path path, boolean writable) throws IOException {
+  public void setWritable(LocalPath path, boolean writable) throws IOException {
     InMemoryContentInfo status;
     synchronized (this) {
       status = scopeLimitedStat(path, true);
@@ -562,14 +562,13 @@
   }
 
   @Override
-  protected boolean isExecutable(Path path) throws IOException {
+  protected boolean isExecutable(LocalPath path) throws IOException {
     InMemoryContentInfo status = scopeLimitedStat(path, true);
     return status.isExecutable();
   }
 
   @Override
-  protected void setExecutable(Path path, boolean executable)
-      throws IOException {
+  protected void setExecutable(LocalPath path, boolean executable) throws IOException {
     synchronized (this) {
       InMemoryContentInfo status = scopeLimitedStat(path, true);
       status.setExecutable(executable);
@@ -577,17 +576,17 @@
   }
 
   @Override
-  public boolean supportsModifications(Path path) {
+  public boolean supportsModifications(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return true;
   }
 
   @Override
-  public boolean supportsHardLinksNatively(Path path) {
+  public boolean supportsHardLinksNatively(LocalPath path) {
     return true;
   }
 
@@ -597,8 +596,8 @@
   }
 
   @Override
-  public boolean createDirectory(Path path) throws IOException {
-    if (path.equals(getRootDirectory())) {
+  public boolean createDirectory(LocalPath path) throws IOException {
+    if (path.isRoot()) {
       throw Error.EACCES.exception(path);
     }
 
@@ -624,9 +623,8 @@
   }
 
   @Override
-  protected void createSymbolicLink(Path path, PathFragment targetFragment)
-      throws IOException {
-    if (path.equals(getRootDirectory())) {
+  protected void createSymbolicLink(LocalPath path, String targetFragment) throws IOException {
+    if (path.isRoot()) {
       throw Error.EACCES.exception(path);
     }
 
@@ -640,7 +638,7 @@
   }
 
   @Override
-  protected PathFragment readSymbolicLink(Path path) throws IOException {
+  protected String readSymbolicLink(LocalPath path) throws IOException {
     InMemoryContentInfo status = scopeLimitedStat(path, false);
     if (status.isSymbolicLink()) {
       Preconditions.checkState(status instanceof InMemoryLinkInfo);
@@ -651,13 +649,12 @@
   }
 
   @Override
-  protected long getFileSize(Path path, boolean followSymlinks)
-      throws IOException {
+  protected long getFileSize(LocalPath path, boolean followSymlinks) throws IOException {
     return stat(path, followSymlinks).getSize();
   }
 
   @Override
-  protected Collection<String> getDirectoryEntries(Path path) throws IOException {
+  protected Collection<String> getDirectoryEntries(LocalPath path) throws IOException {
     synchronized (this) {
       InMemoryDirectoryInfo dirInfo = getDirectory(path);
       FileStatus status = stat(path, false);
@@ -678,8 +675,8 @@
   }
 
   @Override
-  public boolean delete(Path path) throws IOException {
-    if (path.equals(getRootDirectory())) {
+  public boolean delete(LocalPath path) throws IOException {
+    if (path.isRoot()) {
       throw Error.EBUSY.exception(path);
     }
     if (!exists(path, false)) { return false; }
@@ -696,13 +693,12 @@
   }
 
   @Override
-  protected long getLastModifiedTime(Path path, boolean followSymlinks)
-      throws IOException {
+  protected long getLastModifiedTime(LocalPath path, boolean followSymlinks) throws IOException {
     return stat(path, followSymlinks).getLastModifiedTime();
   }
 
   @Override
-  public void setLastModifiedTime(Path path, long newTime) throws IOException {
+  public void setLastModifiedTime(LocalPath path, long newTime) throws IOException {
     synchronized (this) {
       InMemoryContentInfo status = scopeLimitedStat(path, true);
       status.setLastModifiedTime(newTime == -1L ? clock.currentTimeMillis() : newTime);
@@ -710,13 +706,13 @@
   }
 
   @Override
-  protected InputStream getInputStream(Path path) throws IOException {
+  protected InputStream getInputStream(LocalPath path) throws IOException {
     synchronized (this) {
       InMemoryContentInfo status = scopeLimitedStat(path, true);
       if (status.isDirectory()) {
         throw Error.EISDIR.exception(path);
       }
-      if (!path.isReadable()) {
+      if (!isReadable(path)) {
         throw Error.EACCES.exception(path);
       }
       Preconditions.checkState(status instanceof FileInfo);
@@ -725,7 +721,7 @@
   }
 
   /** Creates a new file at the given path and returns its inode. */
-  private InMemoryContentInfo getOrCreateWritableInode(Path path) throws IOException {
+  private InMemoryContentInfo getOrCreateWritableInode(LocalPath path) throws IOException {
     // open(WR_ONLY) of a dangling link writes through the link.  That means
     // that the usual path lookup operations have to behave differently when
     // resolving a path with the intent to create it: instead of failing with
@@ -742,8 +738,7 @@
   }
 
   @Override
-  protected OutputStream getOutputStream(Path path, boolean append)
-      throws IOException {
+  protected OutputStream getOutputStream(LocalPath path, boolean append) throws IOException {
     synchronized (this) {
       InMemoryContentInfo status = getOrCreateWritableInode(path);
       return ((FileInfo) status).getOutputStream(append);
@@ -751,12 +746,11 @@
   }
 
   @Override
-  public void renameTo(Path sourcePath, Path targetPath)
-      throws IOException {
-    if (sourcePath.equals(getRootDirectory())) {
+  public void renameTo(LocalPath sourcePath, LocalPath targetPath) throws IOException {
+    if (sourcePath.isRoot()) {
       throw Error.EACCES.exception(sourcePath);
     }
-    if (targetPath.equals(getRootDirectory())) {
+    if (targetPath.isRoot()) {
       throw Error.EACCES.exception(targetPath);
     }
     synchronized (this) {
@@ -800,11 +794,11 @@
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
+  protected void createFSDependentHardLink(LocalPath linkPath, LocalPath originalPath)
       throws IOException {
 
     // Same check used when creating a symbolic link
-    if (originalPath.equals(getRootDirectory())) {
+    if (originalPath.isRoot()) {
       throw Error.EACCES.exception(originalPath);
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
index 107f319..cee3ebd 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryLinkInfo.java
@@ -16,7 +16,7 @@
 import com.google.devtools.build.lib.clock.Clock;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
-import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.LocalPath;
 
 /**
  * This interface represents a symbolic link to an absolute or relative path,
@@ -25,13 +25,13 @@
 @ThreadSafe @Immutable
 class InMemoryLinkInfo extends InMemoryContentInfo {
 
-  private final PathFragment linkContent;
-  private final PathFragment normalizedLinkContent;
+  private final String linkContent;
+  private final LocalPath normalizedLinkContent;
 
-  InMemoryLinkInfo(Clock clock, PathFragment linkContent) {
+  InMemoryLinkInfo(Clock clock, String linkContent) {
     super(clock);
     this.linkContent = linkContent;
-    this.normalizedLinkContent = linkContent.normalize();
+    this.normalizedLinkContent = LocalPath.create(linkContent);
   }
 
   @Override
@@ -59,18 +59,16 @@
     return linkContent.toString().length();
   }
 
-  /**
-   * Returns the content of the symbolic link.
-   */
-  PathFragment getLinkContent() {
+  /** Returns the content of the symbolic link. */
+  String getLinkContent() {
     return linkContent;
   }
 
   /**
-   * Returns the content of the symbolic link, with ".." and "." removed
-   * (except for the possibility of necessary ".." segments at the beginning).
+   * Returns the content of the symbolic link, with ".." and "." removed (except for the possibility
+   * of necessary ".." segments at the beginning).
    */
-  PathFragment getNormalizedLinkContent() {
+  LocalPath getNormalizedLinkContent() {
     return normalizedLinkContent;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
index af97e27..c98a6c1 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
+import com.google.devtools.build.lib.vfs.LocalPath;
 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;
@@ -32,41 +33,11 @@
 import java.nio.file.LinkOption;
 import java.nio.file.attribute.DosFileAttributes;
 import java.util.Arrays;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.annotation.Nullable;
 
 /** File system implementation for Windows. */
 @ThreadSafe
 public class WindowsFileSystem extends JavaIoFileSystem {
 
-  // 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)
-  @VisibleForTesting
-  static final Predicate<String> SHORT_NAME_MATCHER =
-      new Predicate<String>() {
-        private final Pattern pattern = Pattern.compile("^(.{1,6})~([0-9]{1,6})(\\..{0,3}){0,1}");
-
-        @Override
-        public boolean apply(@Nullable String input) {
-          Matcher m = pattern.matcher(input);
-          return input.length() <= 12
-              && m.matches()
-              && m.groupCount() >= 2
-              && (m.group(1).length() + m.group(2).length()) < 8; // the "~" makes it at most 8
-        }
-      };
-
   /** Resolves DOS-style, shortened path names, returning the last segment's long form. */
   private static final Function<String, String> WINDOWS_SHORT_PATH_RESOLVER =
       path -> {
@@ -114,7 +85,7 @@
       }
 
       String resolvedChild = child;
-      if (parent != null && !parent.isRootDirectory() && SHORT_NAME_MATCHER.apply(child)) {
+      if (parent != null && !parent.isRootDirectory() && WindowsShortPath.isShortPath(child)) {
         String pathString = parent.getPathString();
         if (!pathString.endsWith("/")) {
           pathString += "/";
@@ -286,18 +257,18 @@
   }
 
   @Override
-  public String getFileSystemType(Path path) {
+  public String getFileSystemType(LocalPath 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 {
-    Path targetPath =
-        targetFragment.isAbsolute()
-            ? getPath(targetFragment)
-            : linkPath.getParentDirectory().getRelative(targetFragment);
+  protected void createSymbolicLink(LocalPath linkPath, String targetFragment) throws IOException {
+    LocalPath targetPath = LocalPath.create(targetFragment);
+    if (!targetPath.isAbsolute()) {
+      targetPath = linkPath.getParentDirectory().getRelative(targetPath);
+    }
     try {
       java.nio.file.Path link = getIoFile(linkPath).toPath();
       java.nio.file.Path target = getIoFile(targetPath).toPath();
@@ -317,7 +288,7 @@
   }
 
   @Override
-  public boolean supportsSymbolicLinksNatively(Path path) {
+  public boolean supportsSymbolicLinksNatively(LocalPath path) {
     return false;
   }
 
@@ -343,7 +314,7 @@
   }
 
   @Override
-  protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
+  protected FileStatus stat(LocalPath path, boolean followSymlinks) throws IOException {
     File file = getIoFile(path);
     final DosFileAttributes attributes;
     try {
@@ -402,7 +373,7 @@
   }
 
   @Override
-  protected boolean isDirectory(Path path, boolean followSymlinks) {
+  protected boolean isDirectory(LocalPath path, boolean followSymlinks) {
     if (!followSymlinks) {
       try {
         if (isJunction(getIoFile(path))) {