Replace path implementation.

Path and PathFragment have been replaced with String-based implementations. They are pretty similar, but each method is dissimilar enough that I did not feel sharing code was appropriate.

A summary of changes:

PATH
====

* Subsumes LocalPath (deleted, its tests repurposed)
* Use a simple string to back Path
* Path instances are no longer interned; Reference equality will no longer work
* Always normalized (same as before)
* Some operations will now be slower, like instance compares (which were previously just a reference check)
* Multiple identical paths will now consume more memory since they are not interned

PATH FRAGMENT
=============

* Use a simple string to back PathFragment
* No more segment arrays with interned strings
* Always normalized
* Remove isNormalized
* Replace some isNormalizied uses with containsUpLevelReferences() to check if path fragments try to escape their scope
* To check if user input is normalized, supply static methods on PathFragment to validate the string before constructing a PathFragment
* Because PathFragments are always normalized, we have to replace checks for literal "." from PathFragment#getPathString to PathFragment#getSafePathString. The latter returns "." for the empty string.
* The previous implementation supported efficient segment semantics (segment count, iterating over segments). This is now expensive since we do longer have a segment array.

ARTIFACT
========

* Remove Path instance. It is instead dynamically constructed on request. This is necessary to avoid this CL becoming a memory regression.

RELNOTES: None
PiperOrigin-RevId: 185062932
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Actions.java b/src/main/java/com/google/devtools/build/lib/actions/Actions.java
index 9143826..9b49d5c 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Actions.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Actions.java
@@ -23,7 +23,9 @@
 import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.OsPathPolicy;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -149,7 +151,7 @@
    */
   public static Map<ActionAnalysisMetadata, ArtifactPrefixConflictException>
       findArtifactPrefixConflicts(Map<Artifact, ActionAnalysisMetadata> generatingActions) {
-    TreeMap<PathFragment, Artifact> artifactPathMap = new TreeMap();
+    TreeMap<PathFragment, Artifact> artifactPathMap = new TreeMap<>(comparatorForPrefixConflicts());
     for (Artifact artifact : generatingActions.keySet()) {
       artifactPathMap.put(artifact.getExecPath(), artifact);
     }
@@ -159,18 +161,63 @@
   }
 
   /**
+   * Returns a comparator for use with {@link #findArtifactPrefixConflicts(ActionGraph, SortedMap)}.
+   */
+  public static Comparator<PathFragment> comparatorForPrefixConflicts() {
+    return PathFragmentPrefixComparator.INSTANCE;
+  }
+
+  private static class PathFragmentPrefixComparator implements Comparator<PathFragment> {
+    private static final PathFragmentPrefixComparator INSTANCE = new PathFragmentPrefixComparator();
+
+    @Override
+    public int compare(PathFragment lhs, PathFragment rhs) {
+      // We need to use the OS path policy in case the OS is case insensitive.
+      OsPathPolicy os = OsPathPolicy.getFilePathOs();
+      String str1 = lhs.getPathString();
+      String str2 = rhs.getPathString();
+      int len1 = str1.length();
+      int len2 = str2.length();
+      int n = Math.min(len1, len2);
+      for (int i = 0; i < n; ++i) {
+        char c1 = str1.charAt(i);
+        char c2 = str2.charAt(i);
+        int res = os.compare(c1, c2);
+        if (res != 0) {
+          if (c1 == PathFragment.SEPARATOR_CHAR) {
+            return -1;
+          } else if (c2 == PathFragment.SEPARATOR_CHAR) {
+            return 1;
+          }
+          return res;
+        }
+      }
+      return len1 - len2;
+    }
+  }
+
+  /**
    * Finds Artifact prefix conflicts between generated artifacts. An artifact prefix conflict
    * happens if one action generates an artifact whose path is a prefix of another artifact's path.
    * Those two artifacts cannot exist simultaneously in the output tree.
    *
    * @param actionGraph the {@link ActionGraph} to query for artifact conflicts
-   * @param artifactPathMap a map mapping generated artifacts to their exec paths
+   * @param artifactPathMap a map mapping generated artifacts to their exec paths. The map must be
+   *     sorted using the comparator from {@link #comparatorForPrefixConflicts()}.
    * @return A map between actions that generated the conflicting artifacts and their associated
    *     {@link ArtifactPrefixConflictException}.
    */
   public static Map<ActionAnalysisMetadata, ArtifactPrefixConflictException>
-      findArtifactPrefixConflicts(ActionGraph actionGraph,
-      SortedMap<PathFragment, Artifact> artifactPathMap) {
+      findArtifactPrefixConflicts(
+          ActionGraph actionGraph, SortedMap<PathFragment, Artifact> artifactPathMap) {
+    // You must construct the sorted map using this comparator for the algorithm to work.
+    // The algorithm requires subdirectories to immediately follow parent directories,
+    // before any files in that directory.
+    // Example: "foo", "foo.obj", foo/bar" must be sorted
+    // "foo", "foo/bar", foo.obj"
+    Preconditions.checkArgument(
+        artifactPathMap.comparator() instanceof PathFragmentPrefixComparator,
+        "artifactPathMap must be sorted with PathFragmentPrefixComparator");
     // No actions in graph -- currently happens only in tests. Special-cased because .next() call
     // below is unconditional.
     if (artifactPathMap.isEmpty()) {
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
index 395f5df..964b05d 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -176,7 +176,6 @@
   public static final Predicate<Artifact> MIDDLEMAN_FILTER = input -> !input.isMiddlemanArtifact();
 
   private final int hashCode;
-  private final Path path;
   private final ArtifactRoot root;
   private final PathFragment execPath;
   private final PathFragment rootRelativePath;
@@ -203,35 +202,26 @@
    */
   @VisibleForTesting
   @AutoCodec.Instantiator
-  public Artifact(Path path, ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) {
-    if (root == null || !root.getRoot().contains(path)) {
-      throw new IllegalArgumentException(root + ": illegal root for " + path
-          + " (execPath: " + execPath + ")");
-    }
-    if (execPath == null
-        || execPath.isAbsolute() != root.getRoot().isAbsolute()
-        || !path.asFragment().endsWith(execPath)) {
-      throw new IllegalArgumentException(execPath + ": illegal execPath for " + path
-          + " (root: " + root + ")");
+  public Artifact(ArtifactRoot root, PathFragment execPath, ArtifactOwner owner) {
+    Preconditions.checkNotNull(root);
+    if (execPath == null || execPath.isAbsolute() != root.getRoot().isAbsolute()) {
+      throw new IllegalArgumentException(
+          execPath + ": illegal execPath for " + execPath + " (root: " + root + ")");
     }
     if (execPath.isEmpty()) {
       throw new IllegalArgumentException(
           "it is illegal to create an artifact with an empty execPath");
     }
-    this.hashCode = path.hashCode();
-    this.path = path;
+    this.hashCode = execPath.hashCode();
     this.root = root;
     this.execPath = execPath;
-    // These two lines establish the invariant that
-    // execPath == rootRelativePath <=> execPath.equals(rootRelativePath)
-    // This is important for isSourceArtifact.
-    PathFragment rootRel = root.getRoot().relativize(path);
-    if (!execPath.endsWith(rootRel)) {
-      throw new IllegalArgumentException(execPath + ": illegal execPath doesn't end with "
-          + rootRel + " at " + path + " with root " + root);
+    PathFragment rootExecPath = root.getExecPath();
+    if (rootExecPath.isEmpty()) {
+      this.rootRelativePath = execPath;
+    } else {
+      this.rootRelativePath = execPath.relativeTo(root.getExecPath());
     }
-    this.rootRelativePath = rootRel.equals(execPath) ? execPath : rootRel;
-    this.owner = Preconditions.checkNotNull(owner, path);
+    this.owner = Preconditions.checkNotNull(owner);
   }
 
   /**
@@ -251,8 +241,8 @@
    * <pre>
    */
   @VisibleForTesting
-  public Artifact(Path path, ArtifactRoot root, PathFragment execPath) {
-    this(path, root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE);
+  public Artifact(ArtifactRoot root, PathFragment execPath) {
+    this(root, execPath, ArtifactOwner.NullArtifactOwner.INSTANCE);
   }
 
   /**
@@ -262,7 +252,6 @@
   @VisibleForTesting // Only exists for testing.
   public Artifact(Path path, ArtifactRoot root) {
     this(
-        path,
         root,
         root.getExecPath().getRelative(root.getRoot().relativize(path)),
         ArtifactOwner.NullArtifactOwner.INSTANCE);
@@ -272,14 +261,13 @@
   @VisibleForTesting // Only exists for testing.
   public Artifact(PathFragment rootRelativePath, ArtifactRoot root) {
     this(
-        root.getRoot().getRelative(rootRelativePath),
         root,
         root.getExecPath().getRelative(rootRelativePath),
         ArtifactOwner.NullArtifactOwner.INSTANCE);
   }
 
   public final Path getPath() {
-    return path;
+    return root.getRoot().getRelative(rootRelativePath);
   }
 
   public boolean hasParent() {
@@ -412,9 +400,8 @@
     structField = true,
     doc = "Returns true if this is a source file, i.e. it is not generated."
   )
-  @SuppressWarnings("ReferenceEquality")  // == is an optimization
   public final boolean isSourceArtifact() {
-    return execPath == rootRelativePath;
+    return root.isSourceRoot();
   }
 
   /**
@@ -479,12 +466,8 @@
 
     @VisibleForSerialization
     public SpecialArtifact(
-        Path path,
-        ArtifactRoot root,
-        PathFragment execPath,
-        ArtifactOwner owner,
-        SpecialArtifactType type) {
-      super(path, root, execPath, owner);
+        ArtifactRoot root, PathFragment execPath, ArtifactOwner owner, SpecialArtifactType type) {
+      super(root, execPath, owner);
       this.type = type;
     }
 
@@ -559,17 +542,16 @@
     TreeFileArtifact(
         SpecialArtifact parentTreeArtifact, PathFragment parentRelativePath, ArtifactOwner owner) {
       super(
-          parentTreeArtifact.getPath().getRelative(parentRelativePath),
           parentTreeArtifact.getRoot(),
           parentTreeArtifact.getExecPath().getRelative(parentRelativePath),
           owner);
-      Preconditions.checkState(
+      Preconditions.checkArgument(
           parentTreeArtifact.isTreeArtifact(),
           "The parent of TreeFileArtifact (parent-relative path: %s) is not a TreeArtifact: %s",
           parentRelativePath,
           parentTreeArtifact);
-      Preconditions.checkState(
-          parentRelativePath.isNormalized() && !parentRelativePath.isAbsolute(),
+      Preconditions.checkArgument(
+          !parentRelativePath.containsUplevelReferences() && !parentRelativePath.isAbsolute(),
           "%s is not a proper normalized relative path",
           parentRelativePath);
       this.parentTreeArtifact = parentTreeArtifact;
@@ -668,7 +650,7 @@
     // We don't bother to check root in the equivalence relation, because we
     // assume that no root is an ancestor of another one.
     Artifact that = (Artifact) other;
-    return Objects.equals(this.path, that.path);
+    return Objects.equals(this.execPath, that.execPath) && Objects.equals(this.root, that.root);
   }
 
   @Override
@@ -702,7 +684,7 @@
       return "[" + root + "]" + rootRelativePath;
     } else {
       // Derived Artifact: path and root are under execRoot
-      PathFragment execRoot = trimTail(path.asFragment(), execPath);
+      PathFragment execRoot = trimTail(getPath().asFragment(), execPath);
       return "[["
           + execRoot
           + "]"
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
index 7a0f88b..d23637a 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ArtifactFactory.java
@@ -149,8 +149,7 @@
     Preconditions.checkArgument(
         execPath.isAbsolute() == root.getRoot().isAbsolute(), "%s %s %s", execPath, root, owner);
     Preconditions.checkNotNull(owner, "%s %s", execPath, root);
-    execPath = execPath.normalize();
-    return getArtifact(root.getRoot().getRelative(execPath), root, execPath, owner, null);
+    return getArtifact(root, execPath, owner, null);
   }
 
   @Override
@@ -162,7 +161,7 @@
     Preconditions.checkArgument(!root.isSourceRoot());
     Preconditions.checkArgument(
         rootRelativePath.isAbsolute() == root.getRoot().isAbsolute(), rootRelativePath);
-    Preconditions.checkArgument(rootRelativePath.isNormalized(), rootRelativePath);
+    Preconditions.checkArgument(!rootRelativePath.containsUplevelReferences(), rootRelativePath);
     Preconditions.checkArgument(
         root.getRoot().asPath().startsWith(execRootParent), "%s %s", root, execRootParent);
     Preconditions.checkArgument(
@@ -182,8 +181,7 @@
   public Artifact getDerivedArtifact(
       PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) {
     validatePath(rootRelativePath, root);
-    Path path = root.getRoot().getRelative(rootRelativePath);
-    return getArtifact(path, root, root.getExecPath().getRelative(rootRelativePath), owner, null);
+    return getArtifact(root, root.getExecPath().getRelative(rootRelativePath), owner, null);
   }
 
   /**
@@ -197,13 +195,8 @@
   public Artifact getFilesetArtifact(
       PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) {
     validatePath(rootRelativePath, root);
-    Path path = root.getRoot().getRelative(rootRelativePath);
     return getArtifact(
-        path,
-        root,
-        root.getExecPath().getRelative(rootRelativePath),
-        owner,
-        SpecialArtifactType.FILESET);
+        root, root.getExecPath().getRelative(rootRelativePath), owner, SpecialArtifactType.FILESET);
   }
 
   /**
@@ -216,21 +209,14 @@
   public Artifact getTreeArtifact(
       PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) {
     validatePath(rootRelativePath, root);
-    Path path = root.getRoot().getRelative(rootRelativePath);
     return getArtifact(
-        path,
-        root,
-        root.getExecPath().getRelative(rootRelativePath),
-        owner,
-        SpecialArtifactType.TREE);
+        root, root.getExecPath().getRelative(rootRelativePath), owner, SpecialArtifactType.TREE);
   }
 
   public Artifact getConstantMetadataArtifact(
       PathFragment rootRelativePath, ArtifactRoot root, ArtifactOwner owner) {
     validatePath(rootRelativePath, root);
-    Path path = root.getRoot().getRelative(rootRelativePath);
     return getArtifact(
-        path,
         root,
         root.getExecPath().getRelative(rootRelativePath),
         owner,
@@ -242,7 +228,6 @@
    * root</code> and <code>execPath</code> to the specified values.
    */
   private synchronized Artifact getArtifact(
-      Path path,
       ArtifactRoot root,
       PathFragment execPath,
       ArtifactOwner owner,
@@ -251,7 +236,7 @@
     Preconditions.checkNotNull(execPath);
 
     if (!root.isSourceRoot()) {
-      return createArtifact(path, root, execPath, owner, type);
+      return createArtifact(root, execPath, owner, type);
     }
 
     Artifact artifact = sourceArtifactCache.getArtifact(execPath);
@@ -260,23 +245,22 @@
       // There really should be a safety net that makes it impossible to create two Artifacts
       // with the same exec path but a different Owner, but we also need to reuse Artifacts from
       // previous builds.
-      artifact = createArtifact(path, root, execPath, owner, type);
+      artifact = createArtifact(root, execPath, owner, type);
       sourceArtifactCache.putArtifact(execPath, artifact);
     }
     return artifact;
   }
 
   private Artifact createArtifact(
-      Path path,
       ArtifactRoot root,
       PathFragment execPath,
       ArtifactOwner owner,
       @Nullable SpecialArtifactType type) {
-    Preconditions.checkNotNull(owner, path);
+    Preconditions.checkNotNull(owner);
     if (type == null) {
-      return new Artifact(path, root, execPath, owner);
+      return new Artifact(root, execPath, owner);
     } else {
-      return new Artifact.SpecialArtifact(path, root, execPath, owner, type);
+      return new Artifact.SpecialArtifact(root, execPath, owner, type);
     }
   }
 
@@ -301,7 +285,6 @@
         !relativePath.isEmpty(), "%s %s %s", relativePath, baseExecPath, baseRoot);
     PathFragment execPath =
         baseExecPath == null ? relativePath : baseExecPath.getRelative(relativePath);
-    execPath = execPath.normalize();
     if (execPath.containsUplevelReferences()) {
       // Source exec paths cannot escape the source root.
       return null;
@@ -371,17 +354,16 @@
     ArrayList<PathFragment> unresolvedPaths = new ArrayList<>();
 
     for (PathFragment execPath : execPaths) {
-      PathFragment execPathNormalized = execPath.normalize();
-      if (execPathNormalized.containsUplevelReferences()) {
+      if (execPath.containsUplevelReferences()) {
         // Source exec paths cannot escape the source root.
         result.put(execPath, null);
         continue;
       }
       // First try a quick map lookup to see if the artifact already exists.
-      Artifact a = sourceArtifactCache.getArtifactIfValid(execPathNormalized);
+      Artifact a = sourceArtifactCache.getArtifactIfValid(execPath);
       if (a != null) {
         result.put(execPath, a);
-      } else if (isDerivedArtifact(execPathNormalized)) {
+      } else if (isDerivedArtifact(execPath)) {
         // Don't create an artifact if it's derived.
         result.put(execPath, null);
       } else {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java
index fb5eafd..fb9dc28 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactAction.java
@@ -273,7 +273,7 @@
       if (!line.isEmpty()) {
         PathFragment path = PathFragment.create(line);
 
-        if (!path.isNormalized() || path.isAbsolute()) {
+        if (!PathFragment.isNormalized(line) || path.isAbsolute()) {
           throw new IllegalManifestFileException(
               path + " is not a proper relative path");
         }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index d5aa190..8652028 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -1307,6 +1307,16 @@
     return Objects.hash(isActionsEnabled(), fragments, buildOptions.getOptions());
   }
 
+  public void describe(StringBuilder sb) {
+    sb.append(isActionsEnabled()).append('\n');
+    for (Fragment fragment : fragments.values()) {
+      sb.append(fragment.getClass().getName()).append('\n');
+    }
+    for (String s : buildOptions.toString().split(" ")) {
+      sb.append(s).append('\n');
+    }
+  }
+
   @Override
   public int hashCode() {
     return hashCode;
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java
index a957a00..ea3f887 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/StripPrefixedPath.java
@@ -64,10 +64,9 @@
    * Normalize the path and, if it is absolute, make it relative (e.g., /foo/bar becomes foo/bar).
    */
   private static PathFragment relativize(String path) {
-    PathFragment entryPath = PathFragment.create(path).normalize();
+    PathFragment entryPath = PathFragment.create(path);
     if (entryPath.isAbsolute()) {
-      entryPath = PathFragment.create(entryPath.getSafePathString().substring(
-          entryPath.windowsVolume().length() + 1));
+      entryPath = entryPath.toRelative();
     }
     return entryPath;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
index 71d26fc..0f32e75 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
@@ -124,19 +124,17 @@
       // For symlinks, the "compressed data" is actually the target name.
       int read = reader.getInputStream(entry).read(buffer);
       Preconditions.checkState(read == buffer.length);
-      PathFragment target = PathFragment.create(new String(buffer, Charset.defaultCharset()))
-          .normalize();
-      if (!target.isNormalized()) {
-        PathFragment pointsTo =
-            PathFragment.create(strippedRelativePath.getParentDirectory(), target).normalize();
-        if (!pointsTo.isNormalized()) {
+      PathFragment target = PathFragment.create(new String(buffer, Charset.defaultCharset()));
+      if (target.containsUplevelReferences()) {
+        PathFragment pointsTo = strippedRelativePath.getParentDirectory().getRelative(target);
+        if (pointsTo.containsUplevelReferences()) {
           throw new IOException("Zip entries cannot refer to files outside of their directory: "
               + reader.getFilename() + " has a symlink " + strippedRelativePath + " pointing to "
               + target);
         }
       }
       if (target.isAbsolute()) {
-        target = target.relativeTo(PathFragment.ROOT_DIR);
+        target = target.relativeTo("/");
         target = destinationDirectory.getRelative(target).asFragment();
       }
       outputPath.createSymbolicLink(target);
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
index 50c65c1..21f5ae5 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaSemantics.java
@@ -236,8 +236,8 @@
         if (!isRunfilesEnabled) {
           buffer.append("$(rlocation ");
           PathFragment runfilePath =
-              PathFragment.create(PathFragment.create(workspacePrefix), artifact.getRunfilesPath());
-          buffer.append(runfilePath.normalize().getPathString());
+              PathFragment.create(workspacePrefix).getRelative(artifact.getRunfilesPath());
+          buffer.append(runfilePath.getPathString());
           buffer.append(")");
         } else {
           buffer.append("${RUNPATH}");
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
index 0227f75..8f9ede0 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
@@ -107,8 +107,8 @@
             "ignoring invalid absolute path '" + importsAttr + "'");
         continue;
       }
-      PathFragment importsPath = packageFragment.getRelative(importsAttr).normalize();
-      if (!importsPath.isNormalized()) {
+      PathFragment importsPath = packageFragment.getRelative(importsAttr);
+      if (importsPath.containsUplevelReferences()) {
         ruleContext.attributeError("imports",
             "Path " + importsAttr + " references a path above the execution root");
       }
@@ -239,10 +239,10 @@
     String zipRunfilesPath;
     if (isUnderWorkspace(path)) {
       // If the file is under workspace, add workspace name as prefix
-      zipRunfilesPath = workspaceName.getRelative(path).normalize().toString();
+      zipRunfilesPath = workspaceName.getRelative(path).toString();
     } else {
       // If the file is in external package, strip "external"
-      zipRunfilesPath = path.relativeTo(Label.EXTERNAL_PATH_PREFIX).normalize().toString();
+      zipRunfilesPath = path.relativeTo(Label.EXTERNAL_PATH_PREFIX).toString();
     }
     // We put the whole runfiles tree under the ZIP_RUNFILES_DIRECTORY_NAME directory, by doing this
     // , we avoid the conflict between default workspace name "__main__" and __main__.py file.
diff --git a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
index d07f6a5..8c39215 100644
--- a/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactory.java
@@ -22,6 +22,8 @@
 import com.google.devtools.build.lib.buildeventstream.PathConverter;
 import com.google.devtools.build.lib.vfs.Path;
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 
 /** Factory used to create a Set of BuildEventTransports from BuildEventStreamOptions. */
 public enum BuildEventTransportFactory {
@@ -101,7 +103,41 @@
   private static class NullPathConverter implements PathConverter {
     @Override
     public String apply(Path path) {
-      return path.toURI().toString();
+      return pathToUriString(path.getPathString());
+    }
+  }
+
+  /**
+   * Returns the path encoded as an {@link URI}.
+   *
+   * <p>This concrete implementation returns URIs with "file" as the scheme. For Example: - On Unix
+   * the path "/tmp/foo bar.txt" will be encoded as "file:///tmp/foo%20bar.txt". - On Windows the
+   * path "C:\Temp\Foo Bar.txt" will be encoded as "file:///C:/Temp/Foo%20Bar.txt"
+   *
+   * <p>Implementors extending this class for special filesystems will likely need to override this
+   * method.
+   *
+   * @throws URISyntaxException if the URI cannot be constructed.
+   */
+  static String pathToUriString(String path) {
+    if (!path.startsWith("/")) {
+      // On Windows URI's need to start with a '/'. i.e. C:\Foo\Bar would be file:///C:/Foo/Bar
+      path = "/" + path;
+    }
+    try {
+      return new URI(
+              "file",
+              // Needs to be "" instead of null, so that toString() will append "//" after the
+              // scheme.
+              // We need this for backwards compatibility reasons as some consumers of the BEP are
+              // broken.
+              "",
+              path,
+              null,
+              null)
+          .toString();
+    } catch (URISyntaxException e) {
+      throw new IllegalStateException(e);
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
index 2fbb2a4..054648d 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/Label.java
@@ -345,8 +345,17 @@
     return packageIdentifier.getPackageFragment();
   }
 
-  /** Returns the label as a path fragment, using the package and the label name. */
+  /**
+   * Returns the label as a path fragment, using the package and the label name.
+   *
+   * <p>Make sure that the label refers to a file. Non-file labels do not necessarily have
+   * PathFragment representations.
+   */
   public PathFragment toPathFragment() {
+    // PathFragments are normalized, so if we do this on a non-file target named '.'
+    // then the package would be returned. Detect this and throw.
+    // A target named '.' can never refer to a file.
+    Preconditions.checkArgument(!name.equals("."));
     return packageIdentifier.getPackageFragment().getRelative(name);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java
index ac62a3a..2067b64 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/PackageIdentifier.java
@@ -87,7 +87,7 @@
       // TODO(ulfjack): Remove this when kchodorow@'s exec root rearrangement has been rolled out.
       RepositoryName repository = RepositoryName.create("@" + tofind.getSegment(1));
       return PackageIdentifier.create(repository, tofind.subFragment(2));
-    } else if (!tofind.normalize().isNormalized()) {
+    } else if (tofind.containsUplevelReferences()) {
       RepositoryName repository = RepositoryName.create("@" + tofind.getSegment(1));
       return PackageIdentifier.create(repository, tofind.subFragment(2));
     } else {
@@ -112,9 +112,6 @@
 
   private PackageIdentifier(RepositoryName repository, PathFragment pkgName) {
     this.repository = Preconditions.checkNotNull(repository);
-    if (!pkgName.isNormalized()) {
-      pkgName = pkgName.normalize();
-    }
     this.pkgName = Canonicalizer.fragments().intern(Preconditions.checkNotNull(pkgName));
     this.hashCode = Objects.hash(repository, pkgName);
   }
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index 4bbd3bf..8a1f02a 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -536,7 +536,7 @@
     // stat(2) is executed.
     Path filename = getPackageDirectory().getRelative(targetName);
     String suffix;
-    if (!PathFragment.create(targetName).isNormalized()) {
+    if (!PathFragment.isNormalized(targetName)) {
       // Don't check for file existence in this case because the error message
       // would be confusing and wrong. If the targetName is "foo/bar/.", and
       // there is a directory "foo/bar", it doesn't mean that "//pkg:foo/bar/."
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java
index 8710807..bc314a4 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/RelativePackageNameResolver.java
@@ -72,7 +72,6 @@
     }
 
     PathFragment result = isAbsolute ? relative : offset.getRelative(relative);
-    result = result.normalize();
     if (result.containsUplevelReferences()) {
       throw new InvalidPackageNameException(
           PackageIdentifier.createInMainRepo(pkg),
diff --git a/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java b/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java
index 0e57b17..8a1dfe9 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/TreeNodeRepository.java
@@ -110,8 +110,8 @@
           return false;
         }
         ChildEntry other = (ChildEntry) o;
-        // Pointer comparisons only, because both the Path segments and the TreeNodes are interned.
-        return other.segment == segment && other.child == child;
+        // Pointer comparison for the TreeNode as it is interned
+        return other.segment.equals(segment) && other.child == child;
       }
 
       @Override
@@ -322,7 +322,7 @@
     String segment = segments.get(inputsStart).get(segmentIndex);
     for (int inputIndex = inputsStart; inputIndex < inputsEnd; ++inputIndex) {
       if (inputIndex + 1 == inputsEnd
-          || segment != segments.get(inputIndex + 1).get(segmentIndex)) {
+          || !segment.equals(segments.get(inputIndex + 1).get(segmentIndex))) {
         entries.add(
             new TreeNode.ChildEntry(
                 segment,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
index fa2c96e..e9ae312 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
@@ -545,8 +545,8 @@
             "ignoring invalid absolute path '" + includesAttr + "'");
         continue;
       }
-      PathFragment includesPath = packageFragment.getRelative(includesAttr).normalize();
-      if (!includesPath.isNormalized()) {
+      PathFragment includesPath = packageFragment.getRelative(includesAttr);
+      if (includesPath.containsUplevelReferences()) {
         ruleContext.attributeError("includes",
             "Path references a path above the execution root.");
       }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
index d6464d1..a7c1615 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
@@ -757,15 +757,25 @@
           null);
     }
 
-    PathFragment prefix =
-        ruleContext.attributes().isAttributeValueExplicitlySpecified("include_prefix")
-            ? PathFragment.create(ruleContext.attributes().get("include_prefix", Type.STRING))
-            : null;
+    PathFragment prefix = null;
+    if (ruleContext.attributes().isAttributeValueExplicitlySpecified("include_prefix")) {
+      String prefixAttr = ruleContext.attributes().get("include_prefix", Type.STRING);
+      prefix = PathFragment.create(prefixAttr);
+      if (PathFragment.containsUplevelReferences(prefixAttr)) {
+        ruleContext.attributeError("include_prefix", "should not contain uplevel references");
+      }
+      if (prefix.isAbsolute()) {
+        ruleContext.attributeError("include_prefix", "should be a relative path");
+      }
+    }
 
     PathFragment stripPrefix;
     if (ruleContext.attributes().isAttributeValueExplicitlySpecified("strip_include_prefix")) {
-      stripPrefix =
-          PathFragment.create(ruleContext.attributes().get("strip_include_prefix", Type.STRING));
+      String stripPrefixAttr = ruleContext.attributes().get("strip_include_prefix", Type.STRING);
+      if (PathFragment.containsUplevelReferences(stripPrefixAttr)) {
+        ruleContext.attributeError("strip_include_prefix", "should not contain uplevel references");
+      }
+      stripPrefix = PathFragment.create(stripPrefixAttr);
       if (stripPrefix.isAbsolute()) {
         stripPrefix =
             ruleContext
@@ -791,18 +801,6 @@
           null);
     }
 
-    if (stripPrefix.containsUplevelReferences()) {
-      ruleContext.attributeError("strip_include_prefix", "should not contain uplevel references");
-    }
-
-    if (prefix != null && prefix.containsUplevelReferences()) {
-      ruleContext.attributeError("include_prefix", "should not contain uplevel references");
-    }
-
-    if (prefix != null && prefix.isAbsolute()) {
-      ruleContext.attributeError("include_prefix", "should be a relative path");
-    }
-
     if (ruleContext.hasErrors()) {
       return new PublicHeaders(ImmutableList.<Artifact>of(), ImmutableList.<Artifact>of(), null);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java
index a844ef6..9101e9b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcIncLibrary.java
@@ -98,13 +98,12 @@
 
     // For every source artifact, we compute a virtual artifact that is below the include directory.
     // These are used for include checking.
-    PathFragment prefixFragment = packageFragment.getRelative(expandedIncSymlinkAttr);
-    if (!prefixFragment.isNormalized()) {
+    if (!PathFragment.isNormalized(expandedIncSymlinkAttr)) {
       ruleContext.attributeWarning("prefix", "should not contain '.' or '..' elements");
     }
+    PathFragment prefixFragment = packageFragment.getRelative(expandedIncSymlinkAttr);
     ImmutableSortedMap.Builder<Artifact, Artifact> virtualArtifactMapBuilder =
         ImmutableSortedMap.orderedBy(Artifact.EXEC_PATH_COMPARATOR);
-    prefixFragment = prefixFragment.normalize();
     ImmutableList<Artifact> hdrs = ruleContext.getPrerequisiteArtifacts("hdrs", Mode.TARGET).list();
     for (Artifact src : hdrs) {
       // All declared header files must start with package/targetPrefix.
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java
index 122fc14..5f848b4 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchain.java
@@ -169,10 +169,10 @@
       }
     }
 
-    PathFragment path = PathFragment.create(pathString);
-    if (!path.isNormalized()) {
+    if (!PathFragment.isNormalized(pathString)) {
       throw new InvalidConfigurationException("The include path '" + s + "' is not normalized.");
     }
+    PathFragment path = PathFragment.create(pathString);
     return pathPrefix.getRelative(path);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java
index da8827d..ea12fad 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompilationContext.java
@@ -512,7 +512,7 @@
      * absolute. Before it is stored, the include directory is normalized.
      */
     public Builder addIncludeDir(PathFragment includeDir) {
-      includeDirs.add(includeDir.normalize());
+      includeDirs.add(includeDir);
       return this;
     }
 
@@ -536,7 +536,7 @@
      * is stored, the include directory is normalized.
      */
     public Builder addQuoteIncludeDir(PathFragment quoteIncludeDir) {
-      quoteIncludeDirs.add(quoteIncludeDir.normalize());
+      quoteIncludeDirs.add(quoteIncludeDir);
       return this;
     }
 
@@ -547,7 +547,7 @@
      * is stored, the include directory is normalized.
      */
     public Builder addSystemIncludeDir(PathFragment systemIncludeDir) {
-      systemIncludeDirs.add(systemIncludeDir.normalize());
+      systemIncludeDirs.add(systemIncludeDir);
       return this;
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
index 247d3ef..e462a03 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppCompileActionBuilder.java
@@ -488,8 +488,7 @@
       if (includePath.startsWith(Label.EXTERNAL_PATH_PREFIX)) {
         includePath = includePath.relativeTo(Label.EXTERNAL_PATH_PREFIX);
       }
-      if (includePath.isAbsolute()
-          || !PathFragment.EMPTY_FRAGMENT.getRelative(includePath).normalize().isNormalized()) {
+      if (includePath.isAbsolute() || includePath.containsUplevelReferences()) {
         errorReporter.accept(
             String.format(
                 "The include path '%s' references a path outside of the execution root.",
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
index 058f34d..107a4cc 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
@@ -1381,15 +1381,15 @@
   }
 
   public static PathFragment computeDefaultSysroot(CToolchain toolchain) {
-    PathFragment defaultSysroot =
-        toolchain.getBuiltinSysroot().length() == 0
-            ? null
-            : PathFragment.create(toolchain.getBuiltinSysroot());
-    if ((defaultSysroot != null) && !defaultSysroot.isNormalized()) {
-      throw new IllegalArgumentException(
-          "The built-in sysroot '" + defaultSysroot + "' is not normalized.");
+    String builtInSysroot = toolchain.getBuiltinSysroot();
+    if (builtInSysroot.isEmpty()) {
+      return null;
     }
-    return defaultSysroot;
+    if (!PathFragment.isNormalized(builtInSysroot)) {
+      throw new IllegalArgumentException(
+          "The built-in sysroot '" + builtInSysroot + "' is not normalized.");
+    }
+    return PathFragment.create(builtInSysroot);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java
index 135b70b..91f9502 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppToolchainInfo.java
@@ -794,11 +794,11 @@
       CToolchain toolchain, PathFragment crosstoolTopPathFragment) {
     Map<String, PathFragment> toolPathsCollector = Maps.newHashMap();
     for (CrosstoolConfig.ToolPath tool : toolchain.getToolPathList()) {
-      PathFragment path = PathFragment.create(tool.getPath());
-      if (!path.isNormalized()) {
-        throw new IllegalArgumentException(
-            "The include path '" + tool.getPath() + "' is not normalized.");
+      String pathStr = tool.getPath();
+      if (!PathFragment.isNormalized(pathStr)) {
+        throw new IllegalArgumentException("The include path '" + pathStr + "' is not normalized.");
       }
+      PathFragment path = PathFragment.create(pathStr);
       toolPathsCollector.put(tool.getName(), crosstoolTopPathFragment.getRelative(path));
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
index 458ac57..b2394ca 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
@@ -505,9 +505,9 @@
 
     if (!javaExecutable.isAbsolute()) {
       javaExecutable =
-          PathFragment.create(PathFragment.create(ruleContext.getWorkspaceName()), javaExecutable);
+          PathFragment.create(ruleContext.getWorkspaceName()).getRelative(javaExecutable);
     }
-    return javaExecutable.normalize().getPathString();
+    return javaExecutable.getPathString();
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java b/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java
index 7f6437e..e15c748 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/nativedeps/NativeDepsHelper.java
@@ -312,7 +312,7 @@
     PathFragment libParentDir =
         relativePath.replaceName(lib.getExecPath().getParentDirectory().getBaseName());
     String libName = lib.getExecPath().getBaseName();
-    return PathFragment.create(libParentDir, PathFragment.create(libName));
+    return libParentDir.getRelative(libName);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
index 2e84f6f..e73c665 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
@@ -188,8 +188,7 @@
 
     makeVariableContext.validatePathRoot(pathString);
 
-    PathFragment pathFragment = PathFragment.create(pathString);
-    if (!pathFragment.isNormalized()) {
+    if (!PathFragment.isNormalized(pathString)) {
       throw ruleContext.throwWithAttributeError(
           AppleStubBinaryRule.XCENV_BASED_PATH_ATTR, PATH_NOT_NORMALIZED_ERROR);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
index 39c28b7..f3c79b2 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
@@ -403,7 +403,7 @@
           Iterables.filter(includes(), Predicates.not(PathFragment::isAbsolute));
       for (PathFragment include : relativeIncludes) {
         for (PathFragment rootFragment : rootFragments) {
-          paths.add(rootFragment.getRelative(include).normalize());
+          paths.add(rootFragment.getRelative(include));
         }
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java
index 27c1f67..61bbb37 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcLibrary.java
@@ -130,7 +130,7 @@
     // We add another header search path with gen root if we have generated sources to translate.
     for (Artifact sourceToTranslate : sourcesToTranslate) {
       if (!sourceToTranslate.isSourceArtifact()) {
-        headerSearchPaths.add(PathFragment.create(objcFileRootExecPath, genRoot));
+        headerSearchPaths.add(objcFileRootExecPath.getRelative(genRoot));
         return headerSearchPaths.build();
       }
     }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java
index 1a7e1e2..b3c6d7e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ProtobufSupport.java
@@ -515,8 +515,7 @@
     // of dependers.
     PathFragment rootRelativeOutputDir = ruleContext.getUniqueDirectory(UNIQUE_DIRECTORY_NAME);
 
-    return PathFragment.create(
-        buildConfiguration.getBinDirectory().getExecPath(), rootRelativeOutputDir);
+    return buildConfiguration.getBinDirectory().getExecPath().getRelative(rootRelativeOutputDir);
   }
 
   private Iterable<Artifact> getGeneratedProtoOutputs(
@@ -526,9 +525,8 @@
       String protoFileName = FileSystemUtils.removeExtension(protoFile.getFilename());
       String generatedOutputName = attributes.getGeneratedProtoFilename(protoFileName, true);
 
-      PathFragment generatedFilePath = PathFragment.create(
-          protoFile.getRootRelativePath().getParentDirectory(),
-          PathFragment.create(generatedOutputName));
+      PathFragment generatedFilePath =
+          protoFile.getRootRelativePath().getParentDirectory().getRelative(generatedOutputName);
 
       PathFragment outputFile = FileSystemUtils.appendExtension(generatedFilePath, extension);
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
index c4b1e5f..87cddd7 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/DumpCommand.java
@@ -33,7 +33,6 @@
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor.RuleStat;
 import com.google.devtools.build.lib.util.ExitCode;
-import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.common.options.EnumConverter;
 import com.google.devtools.common.options.Option;
 import com.google.devtools.common.options.OptionDocumentationCategory;
@@ -207,8 +206,9 @@
       }
 
       if (dumpOptions.dumpVfs) {
+        // TODO(b/72498697): Remove this flag
         out.println("Filesystem cache");
-        FileSystemUtils.dump(env.getOutputBase().getFileSystem(), out);
+        out.println("dump --vfs is no longer meaningful");
         out.println();
       }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java
index 6d6d2eb..4c699ab 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/LocalRepositoryLookupFunction.java
@@ -217,8 +217,7 @@
           String path = (String) rule.getAttributeContainer().getAttr("path");
           return Optional.of(
               LocalRepositoryLookupValue.success(
-                  RepositoryName.create("@" + rule.getName()),
-                  PathFragment.create(path).normalize()));
+                  RepositoryName.create("@" + rule.getName()), PathFragment.create(path)));
         } catch (LabelSyntaxException e) {
           // This shouldn't be possible if the rule name is valid, and it should already have been
           // validated.
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index 6da94fd..6114f1d 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -814,7 +814,13 @@
     Set<SkyKey> containingPkgLookupKeys = Sets.newHashSet();
     Map<Target, SkyKey> targetToKey = new HashMap<>();
     for (Target target : pkgBuilder.getTargets()) {
-      PathFragment dir = target.getLabel().toPathFragment().getParentDirectory();
+      PathFragment dir = getContainingDirectory(target.getLabel());
+      if (dir == null) {
+        throw new IllegalStateException(
+            String.format(
+                "Null pkg for label %s as path fragment %s in pkg %s",
+                target.getLabel(), target.getLabel().getPackageFragment(), pkgId));
+      }
       PackageIdentifier dirId = PackageIdentifier.create(pkgId.getRepository(), dir);
       if (dir.equals(pkgId.getPackageFragment())) {
         continue;
@@ -825,7 +831,7 @@
     }
     Map<Label, SkyKey> subincludeToKey = new HashMap<>();
     for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) {
-      PathFragment dir = subincludeLabel.toPathFragment().getParentDirectory();
+      PathFragment dir = getContainingDirectory(subincludeLabel);
       PackageIdentifier dirId = PackageIdentifier.create(pkgId.getRepository(), dir);
       if (dir.equals(pkgId.getPackageFragment())) {
         continue;
@@ -870,6 +876,12 @@
     }
   }
 
+  private static PathFragment getContainingDirectory(Label label) {
+    PathFragment pkg = label.getPackageFragment();
+    String name = label.getName();
+    return name.equals(".") ? pkg : pkg.getRelative(name).getParentDirectory();
+  }
+
   @Nullable
   private static ContainingPackageLookupValue
   getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
index 97d257c..ff12e07 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -266,7 +266,8 @@
           ConcurrentMap<ActionAnalysisMetadata, ConflictException> badActionMap)
           throws InterruptedException {
     MutableActionGraph actionGraph = new MapBasedActionGraph(actionKeyContext);
-    ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap = new ConcurrentSkipListMap<>();
+    ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap =
+        new ConcurrentSkipListMap<>(Actions.comparatorForPrefixConflicts());
     // Action graph construction is CPU-bound.
     int numJobs = Runtime.getRuntime().availableProcessors();
     // No great reason for expecting 5000 action lookup values, but not worth counting size of
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
index 2aeba41..1758fb3 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
@@ -57,7 +57,7 @@
     if (label.getName().contains("/")) {
       // This target is in a subdirectory, therefore it could potentially be invalidated by
       // a new BUILD file appearing in the hierarchy.
-      PathFragment containingDirectory = label.toPathFragment().getParentDirectory();
+      PathFragment containingDirectory = getContainingDirectory(label);
       ContainingPackageLookupValue containingPackageLookupValue;
       try {
         PackageIdentifier newPkgId = PackageIdentifier.create(
@@ -109,6 +109,12 @@
     return TargetMarkerValue.TARGET_MARKER_INSTANCE;
   }
 
+  private static PathFragment getContainingDirectory(Label label) {
+    PathFragment pkg = label.getPackageFragment();
+    String name = label.getName();
+    return name.equals(".") ? pkg : pkg.getRelative(name).getParentDirectory();
+  }
+
   @Override
   public String extractTag(SkyKey skyKey) {
     return Label.print((Label) skyKey.argument());
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
index 783899a..f54522f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
@@ -182,8 +182,7 @@
       PathFragment pathToExplode, ImmutableSet.Builder<PathFragment> valuesBuilder)
       throws IOException {
     for (Path subpath : treeArtifact.getPath().getRelative(pathToExplode).getDirectoryEntries()) {
-      PathFragment canonicalSubpathFragment =
-          pathToExplode.getChild(subpath.getBaseName()).normalize();
+      PathFragment canonicalSubpathFragment = pathToExplode.getChild(subpath.getBaseName());
       if (subpath.isDirectory()) {
         explodeDirectory(treeArtifact,
             pathToExplode.getChild(subpath.getBaseName()), valuesBuilder);
@@ -202,7 +201,7 @@
         // TreeArtifact into a/b/outside_dir.
         PathFragment intermediatePath = canonicalSubpathFragment.getParentDirectory();
         for (String pathSegment : linkTarget.getSegments()) {
-          intermediatePath = intermediatePath.getRelative(pathSegment).normalize();
+          intermediatePath = intermediatePath.getRelative(pathSegment);
           if (intermediatePath.containsUplevelReferences()) {
             String errorMessage = String.format(
                 "A TreeArtifact may not contain relative symlinks whose target paths traverse "
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java
index e8c9ad2..26fc565 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkImports.java
@@ -173,7 +173,7 @@
 
     @Override
     public PathFragment asPathFragment() {
-      return PathFragment.create(PathFragment.ROOT_DIR).getRelative(importLabel.toPathFragment());
+      return PathFragment.create("/").getRelative(importLabel.toPathFragment());
     }
 
     @Override
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 033a6a6..d5a74eb 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
@@ -326,7 +326,7 @@
   @Override
   protected void createSymbolicLink(Path linkPath, PathFragment targetFragment)
       throws IOException {
-    NativePosixFiles.symlink(targetFragment.toString(), linkPath.toString());
+    NativePosixFiles.symlink(targetFragment.getSafePathString(), linkPath.toString());
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/BUILD b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
index f7a57be..b829858 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/vfs/BUILD
@@ -10,8 +10,9 @@
     "Canonicalizer.java",
     "PathFragment.java",
     "PathFragmentSerializationProxy.java",
-    "UnixPathFragment.java",
-    "WindowsPathFragment.java",
+    "OsPathPolicy.java",
+    "UnixOsPathPolicy.java",
+    "WindowsOsPathPolicy.java",
 ]
 
 java_library(
@@ -25,6 +26,8 @@
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
+        "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path",
+        "//src/main/java/com/google/devtools/build/lib/windows/jni",
         "//third_party:guava",
         "//third_party/protobuf:protobuf_java",
     ],
@@ -47,7 +50,6 @@
         ":pathfragment",
         "//src/main/java/com/google/devtools/build/lib:base-util",
         "//src/main/java/com/google/devtools/build/lib:filetype",
-        "//src/main/java/com/google/devtools/build/lib:os_util",
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib/clock",
         "//src/main/java/com/google/devtools/build/lib/concurrent",
@@ -55,8 +57,6 @@
         "//src/main/java/com/google/devtools/build/lib/shell",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
-        "//src/main/java/com/google/devtools/build/lib/windows:windows_short_path",
-        "//src/main/java/com/google/devtools/build/lib/windows/jni",
         "//src/main/java/com/google/devtools/common/options",
         "//third_party:guava",
         "//third_party:jsr305",
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/FileSystem.java
index aaaacd9..38da81c 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
@@ -23,7 +23,6 @@
 import com.google.common.io.CharStreams;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.vfs.Dirent.Type;
-import com.google.devtools.build.lib.vfs.Path.PathFactory;
 import com.google.devtools.common.options.EnumConverter;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -84,25 +83,6 @@
     return digestFunction;
   }
 
-  private enum UnixPathFactory implements PathFactory {
-    INSTANCE {
-      @Override
-      public Path createRootPath(FileSystem filesystem) {
-        return new Path(filesystem, PathFragment.ROOT_DIR, null);
-      }
-
-      @Override
-      public Path createChildPath(Path parent, String childName) {
-        return new Path(parent.getFileSystem(), childName, parent);
-      }
-
-      @Override
-      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.
    */
@@ -112,53 +92,23 @@
     }
   }
 
-  /** Lazy-initialized on first access, always use {@link FileSystem#getRootDirectory} */
-  private volatile Path rootPath;
-
   private final Root absoluteRoot = new Root.AbsoluteRoot(this);
 
-  /** Returns filesystem-specific path factory. */
-  protected PathFactory getPathFactory() {
-    return UnixPathFactory.INSTANCE;
+  /**
+   * 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 path) {
+    return Path.create(path, this);
   }
 
-  /**
-   * 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.
-   */
-  public Path getPath(PathFragment pathName) {
-    if (!pathName.isAbsolute()) {
-      throw new IllegalArgumentException(pathName.getPathString()  + " (not an absolute path)");
-    }
-    return getRootDirectory().getRelative(pathName);
-  }
-
-  /**
-   * Returns a path representing the root directory of the current file system.
-   */
-  public final Path getRootDirectory() {
-    if (rootPath == null) {
-      synchronized (this) {
-        if (rootPath == null) {
-          rootPath = getPathFactory().createRootPath(this);
-        }
-      }
-    }
-    return rootPath;
+  /** Returns an absolute path instance, given an absolute path fragment. */
+  public Path getPath(PathFragment pathFragment) {
+    Preconditions.checkArgument(pathFragment.isAbsolute());
+    return Path.createAlreadyNormalized(
+        pathFragment.getPathString(), pathFragment.getDriveStrLength(), this);
   }
 
   final Root getAbsoluteRoot() {
@@ -401,7 +351,7 @@
       throw new IOException(naive + " (Too many levels of symbolic links)");
     }
     if (linkTarget.isAbsolute()) {
-      dir = getRootDirectory();
+      dir = getPath(linkTarget.getDriveStr());
     }
     for (String name : linkTarget.segments()) {
       if (name.equals(".") || name.isEmpty()) {
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 adeb9c8..b7fd8d2 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
@@ -25,7 +25,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.PrintStream;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -90,25 +89,13 @@
 
     return a;
   }
-  /**
-   * Returns a path fragment from a given from-dir to a given to-path. May be
-   * either a short relative path "foo/bar", an up'n'over relative path
-   * "../../foo/bar" or an absolute path.
-   */
-  public static PathFragment relativePath(Path fromDir, Path to) {
-    if (to.getFileSystem() != fromDir.getFileSystem()) {
-      throw new IllegalArgumentException("fromDir and to must be on the same FileSystem");
-    }
-
-    return relativePath(fromDir.asFragment(), to.asFragment());
-  }
 
   /**
    * Returns a path fragment from a given from-dir to a given to-path.
    */
   public static PathFragment relativePath(PathFragment fromDir, PathFragment to) {
     if (to.equals(fromDir)) {
-      return PathFragment.create(".");  // same dir, just return '.'
+      return PathFragment.EMPTY_FRAGMENT;
     }
     if (to.startsWith(fromDir)) {
       return to.relativeTo(fromDir);  // easy case--it's a descendant
@@ -883,30 +870,6 @@
   }
 
   /**
-   * Dumps diagnostic information about the specified filesystem to {@code out}.
-   * This is the implementation of the filesystem part of the 'blaze dump'
-   * command. It lives here, rather than in DumpCommand, because it requires
-   * privileged access to members of this package.
-   *
-   * <p>Its results are unspecified and MUST NOT be interpreted programmatically.
-   */
-  public static void dump(FileSystem fs, final PrintStream out) {
-    // Unfortunately there's no "letrec" for anonymous functions so we have to
-    // (a) name the function, (b) put it in a box and (c) use List not array
-    // because of the generic type.  *sigh*.
-    final List<Predicate<Path>> dumpFunction = new ArrayList<>();
-    dumpFunction.add(
-        child -> {
-          Path path = child;
-          out.println("  " + path + " (" + path.toDebugString() + ")");
-          path.applyToChildren(dumpFunction.get(0));
-          return false;
-        });
-
-    fs.getRootDirectory().applyToChildren(dumpFunction.get(0));
-  }
-
-  /**
    * Returns the type of the file system path belongs to.
    */
   public static String getFileSystem(Path path) {
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 5bba535..f5b0220 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
@@ -281,7 +281,7 @@
       throws IOException {
     java.nio.file.Path nioPath = getNioPath(linkPath);
     try {
-      Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getPathString()));
+      Files.createSymbolicLink(nioPath, Paths.get(targetFragment.getSafePathString()));
     } catch (java.nio.file.FileAlreadyExistsException e) {
       throw new IOException(linkPath + ERR_FILE_EXISTS);
     } catch (java.nio.file.AccessDeniedException e) {
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
deleted file mode 100644
index a32a4e3..0000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/LocalPath.java
+++ /dev/null
@@ -1,715 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
-import com.google.devtools.build.lib.util.OS;
-import com.google.devtools.build.lib.windows.WindowsShortPath;
-import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.annotation.Nullable;
-
-/**
- * A local file path representing a file on the host machine. You should use this when you want to
- * access local files via the file system.
- *
- * <p>Paths are either absolute or relative.
- *
- * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
- * slashes ('/') removed, and any trailing slash also removed. The current implementation does not
- * touch the incoming path string unless the string actually needs to be normalized.
- *
- * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
- * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as
- * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like
- * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
- * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
- * normalization.
- *
- * <p>Mac and Windows file paths are case insensitive. Case is preserved.
- *
- * <p>This class is replaces {@link Path} as the way to access the host machine's file system.
- * Developers should use this class instead of {@link Path}.
- */
-public final class LocalPath implements Comparable<LocalPath> {
-  private static final OsPathPolicy DEFAULT_OS = createFilePathOs();
-
-  public static final LocalPath EMPTY = create("");
-
-  private final String path;
-  private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
-  private final OsPathPolicy os;
-
-  /** Creates a local path that is specific to the host OS. */
-  public static LocalPath create(String path) {
-    return createWithOs(path, DEFAULT_OS);
-  }
-
-  @VisibleForTesting
-  static LocalPath createWithOs(String path, OsPathPolicy os) {
-    Preconditions.checkNotNull(path);
-    int normalizationLevel = os.needsToNormalize(path);
-    String normalizedPath = os.normalize(path, normalizationLevel);
-    int driveStrLength = os.getDriveStrLength(normalizedPath);
-    return new LocalPath(normalizedPath, driveStrLength, os);
-  }
-
-  /** This method expects path to already be normalized. */
-  private LocalPath(String path, int driveStrLength, OsPathPolicy os) {
-    this.path = Preconditions.checkNotNull(path);
-    this.driveStrLength = driveStrLength;
-    this.os = Preconditions.checkNotNull(os);
-  }
-
-  public String getPathString() {
-    return path;
-  }
-
-  /**
-   * If called on a {@link LocalPath} instance for a mount name (eg. '/' or 'C:/'), the empty string
-   * is returned.
-   */
-  public String getBaseName() {
-    int lastSeparator = path.lastIndexOf(os.getSeparator());
-    return lastSeparator < driveStrLength
-        ? path.substring(driveStrLength)
-        : path.substring(lastSeparator + 1);
-  }
-
-  /**
-   * Returns a {@link LocalPath} instance representing the relative path between this {@link
-   * LocalPath} and the given {@link LocalPath}.
-   *
-   * <pre>
-   *   Example:
-   *
-   *   LocalPath.create("/foo").getRelative(LocalPath.create("bar/baz"))
-   *   -> "/foo/bar/baz"
-   * </pre>
-   *
-   * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve
-   * symlinks.
-   */
-  public LocalPath getRelative(LocalPath other) {
-    Preconditions.checkNotNull(other);
-    Preconditions.checkArgument(os == other.os);
-    return getRelative(other.getPathString(), other.driveStrLength);
-  }
-
-  /**
-   * Returns a {@link LocalPath} instance representing the relative path between this {@link
-   * LocalPath} and the given path.
-   *
-   * <p>See {@link #getRelative(LocalPath)} for details.
-   */
-  public LocalPath getRelative(String other) {
-    Preconditions.checkNotNull(other);
-    return getRelative(other, os.getDriveStrLength(other));
-  }
-
-  private LocalPath getRelative(String other, int otherDriveStrLength) {
-    if (path.isEmpty()) {
-      return create(other);
-    }
-    if (other.isEmpty()) {
-      return this;
-    }
-    // Note that even if other came from a LocalPath instance we still might
-    // need to normalize the result if (for instance) other is a path that
-    // starts with '..'
-    int normalizationLevel = os.needsToNormalize(other);
-    // This is an absolute path, simply return it
-    if (otherDriveStrLength > 0) {
-      String normalizedPath = os.normalize(other, normalizationLevel);
-      return new LocalPath(normalizedPath, otherDriveStrLength, os);
-    }
-    String newPath;
-    if (path.length() == driveStrLength) {
-      newPath = path + other;
-    } else {
-      newPath = path + '/' + other;
-    }
-    newPath = os.normalize(newPath, normalizationLevel);
-    return new LocalPath(newPath, driveStrLength, os);
-  }
-
-  /**
-   * Returns the parent directory of this {@link LocalPath}.
-   *
-   * <p>If this is called on an single directory for a relative path, this returns an empty relative
-   * path. If it's called on a root (like '/') or the empty string, it returns null.
-   */
-  @Nullable
-  public LocalPath getParentDirectory() {
-    int lastSeparator = path.lastIndexOf(os.getSeparator());
-
-    // For absolute paths we need to specially handle when we hit root
-    // Relative paths can't hit this path as driveStrLength == 0
-    if (driveStrLength > 0) {
-      if (lastSeparator < driveStrLength) {
-        if (path.length() > driveStrLength) {
-          String newPath = path.substring(0, driveStrLength);
-          return new LocalPath(newPath, driveStrLength, os);
-        } else {
-          return null;
-        }
-      }
-    } else {
-      if (lastSeparator == -1) {
-        if (!path.isEmpty()) {
-          return EMPTY;
-        } else {
-          return null;
-        }
-      }
-    }
-    String newPath = path.substring(0, lastSeparator);
-    return new LocalPath(newPath, driveStrLength, os);
-  }
-
-  /**
-   * Returns the {@link LocalPath} relative to the base {@link LocalPath}.
-   *
-   * <p>For example, <code>LocalPath.create("foo/bar/wiz").relativeTo(LocalPath.create("foo"))
-   * </code> returns <code>LocalPath.create("bar/wiz")</code>.
-   *
-   * <p>If the {@link LocalPath} is not a child of the passed {@link LocalPath} an {@link
-   * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
-   * LocalPath} instances aren't both absolute or both relative.
-   */
-  public LocalPath relativeTo(LocalPath base) {
-    Preconditions.checkNotNull(base);
-    Preconditions.checkArgument(os == base.os);
-    if (isAbsolute() != base.isAbsolute()) {
-      throw new IllegalArgumentException(
-          "Cannot relativize an absolute and a non-absolute path pair");
-    }
-    String basePath = base.path;
-    if (!os.startsWith(path, basePath)) {
-      throw new IllegalArgumentException(
-          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
-    }
-    int bn = basePath.length();
-    if (bn == 0) {
-      return this;
-    }
-    if (path.length() == bn) {
-      return EMPTY;
-    }
-    final int lastSlashIndex;
-    if (basePath.charAt(bn - 1) == '/') {
-      lastSlashIndex = bn - 1;
-    } else {
-      lastSlashIndex = bn;
-    }
-    if (path.charAt(lastSlashIndex) != '/') {
-      throw new IllegalArgumentException(
-          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
-    }
-    String newPath = path.substring(lastSlashIndex + 1);
-    return new LocalPath(newPath, 0 /* Always a relative path */, os);
-  }
-
-  /**
-   * Splits a path into its constituent parts. The root is not included. This is an inefficient
-   * operation and should be avoided.
-   */
-  public String[] split() {
-    String[] segments = path.split("/");
-    if (driveStrLength > 0) {
-      // String#split("/") for some reason returns a zero-length array
-      // String#split("/hello") returns a 2-length array, so this makes little sense
-      if (segments.length == 0) {
-        return segments;
-      }
-      return Arrays.copyOfRange(segments, 1, segments.length);
-    }
-    return segments;
-  }
-
-  /**
-   * Returns whether this path is an ancestor of another path.
-   *
-   * <p>A path is considered an ancestor of itself.
-   *
-   * <p>An absolute path can never be an ancestor of a relative path, and vice versa.
-   */
-  public boolean startsWith(LocalPath other) {
-    Preconditions.checkNotNull(other);
-    Preconditions.checkArgument(os == other.os);
-    if (other.path.length() > path.length()) {
-      return false;
-    }
-    if (driveStrLength != other.driveStrLength) {
-      return false;
-    }
-    if (!os.startsWith(path, other.path)) {
-      return false;
-    }
-    return path.length() == other.path.length()
-        || other.path.length() == driveStrLength
-        || path.charAt(other.path.length()) == os.getSeparator();
-  }
-
-  public boolean isAbsolute() {
-    return driveStrLength > 0;
-  }
-
-  @Override
-  public String toString() {
-    return path;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    return os.compare(this.path, ((LocalPath) o).path) == 0;
-  }
-
-  @Override
-  public int hashCode() {
-    return os.hashPath(this.path);
-  }
-
-  @Override
-  public int compareTo(LocalPath o) {
-    return os.compare(this.path, o.path);
-  }
-
-  /**
-   * An interface class representing the differences in path style between different OSs.
-   *
-   * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc.
-   */
-  @VisibleForTesting
-  interface OsPathPolicy {
-    int NORMALIZED = 0; // Path is normalized
-    int NEEDS_NORMALIZE = 1; // Path requires normalization
-
-    /** Returns required normalization level, passed to {@link #normalize}. */
-    int needsToNormalize(String path);
-
-    /**
-     * Normalizes the passed string according to the passed normalization level.
-     *
-     * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize}
-     */
-    String normalize(String path, int normalizationLevel);
-
-    /**
-     * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'.
-     *
-     * <p>If the path is relative, 0 is returned
-     */
-    int getDriveStrLength(String path);
-
-    /** Compares two path strings, using the given OS case sensitivity. */
-    int compare(String s1, String s2);
-
-    /** Computes the hash code for a path string. */
-    int hashPath(String s);
-
-    /**
-     * Returns whether the passed string starts with the given prefix, given the OS case
-     * sensitivity.
-     *
-     * <p>This is a pure string operation and doesn't need to worry about matching path segments.
-     */
-    boolean startsWith(String path, String prefix);
-
-    char getSeparator();
-
-    boolean isCaseSensitive();
-  }
-
-  @VisibleForTesting
-  static class UnixOsPathPolicy implements OsPathPolicy {
-
-    @Override
-    public int needsToNormalize(String path) {
-      int n = path.length();
-      int dotCount = 0;
-      char prevChar = 0;
-      for (int i = 0; i < n; i++) {
-        char c = path.charAt(i);
-        if (c == '/') {
-          if (prevChar == '/') {
-            return NEEDS_NORMALIZE;
-          }
-          if (dotCount == 1 || dotCount == 2) {
-            return NEEDS_NORMALIZE;
-          }
-        }
-        dotCount = c == '.' ? dotCount + 1 : 0;
-        prevChar = c;
-      }
-      if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
-        return NEEDS_NORMALIZE;
-      }
-      return NORMALIZED;
-    }
-
-    @Override
-    public String normalize(String path, int normalizationLevel) {
-      if (normalizationLevel == NORMALIZED) {
-        return path;
-      }
-      if (path.isEmpty()) {
-        return path;
-      }
-      boolean isAbsolute = path.charAt(0) == '/';
-      String[] segments = path.split("/+");
-      int segmentCount = removeRelativePaths(segments, isAbsolute ? 1 : 0);
-      StringBuilder sb = new StringBuilder(path.length());
-      if (isAbsolute) {
-        sb.append('/');
-      }
-      for (int i = 0; i < segmentCount; ++i) {
-        sb.append(segments[i]);
-        sb.append('/');
-      }
-      if (segmentCount > 0) {
-        sb.deleteCharAt(sb.length() - 1);
-      }
-      return sb.toString();
-    }
-
-    @Override
-    public int getDriveStrLength(String path) {
-      if (path.length() == 0) {
-        return 0;
-      }
-      return (path.charAt(0) == '/') ? 1 : 0;
-    }
-
-    @Override
-    public int compare(String s1, String s2) {
-      return s1.compareTo(s2);
-    }
-
-    @Override
-    public int hashPath(String s) {
-      return s.hashCode();
-    }
-
-    @Override
-    public boolean startsWith(String path, String prefix) {
-      return path.startsWith(prefix);
-    }
-
-    @Override
-    public char getSeparator() {
-      return '/';
-    }
-
-    @Override
-    public boolean isCaseSensitive() {
-      return true;
-    }
-  }
-
-  /** Mac is a unix file system that is case insensitive. */
-  @VisibleForTesting
-  static class MacOsPathPolicy extends UnixOsPathPolicy {
-    @Override
-    public int compare(String s1, String s2) {
-      return s1.compareToIgnoreCase(s2);
-    }
-
-    @Override
-    public int hashPath(String s) {
-      return s.toLowerCase().hashCode();
-    }
-
-    @Override
-    public boolean isCaseSensitive() {
-      return false;
-    }
-  }
-
-  @VisibleForTesting
-  static class WindowsOsPathPolicy implements OsPathPolicy {
-
-    private static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1;
-
-    // msys root, used to resolve paths from msys starting with "/"
-    private static final AtomicReference<String> UNIX_ROOT = new AtomicReference<>(null);
-    private final ShortPathResolver shortPathResolver;
-
-    interface ShortPathResolver {
-      String resolveShortPath(String path);
-    }
-
-    static class DefaultShortPathResolver implements ShortPathResolver {
-      @Override
-      public String resolveShortPath(String path) {
-        try {
-          return WindowsFileOperations.getLongPath(path);
-        } catch (IOException e) {
-          return path;
-        }
-      }
-    }
-
-    WindowsOsPathPolicy() {
-      this(new DefaultShortPathResolver());
-    }
-
-    WindowsOsPathPolicy(ShortPathResolver shortPathResolver) {
-      this.shortPathResolver = shortPathResolver;
-    }
-
-    @Override
-    public int needsToNormalize(String path) {
-      int n = path.length();
-      int normalizationLevel = 0;
-      // Check for unix path
-      if (n > 0 && path.charAt(0) == '/') {
-        normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
-      }
-      int dotCount = 0;
-      char prevChar = 0;
-      int segmentBeginIndex = 0; // The start index of the current path index
-      boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test
-      for (int i = 0; i < n; i++) {
-        char c = path.charAt(i);
-        if (c == '/' || c == '\\') {
-          if (c == '\\') {
-            normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
-          }
-          // No need to check for '\' here because that already causes normalization
-          if (prevChar == '/') {
-            normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
-          }
-          if (dotCount == 1 || dotCount == 2) {
-            normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
-          }
-          if (segmentHasShortPathChar) {
-            if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) {
-              normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
-            }
-          }
-          segmentBeginIndex = i + 1;
-          segmentHasShortPathChar = false;
-        } else if (c == '~') {
-          // This path segment might be a Windows short path segment
-          segmentHasShortPathChar = true;
-        }
-        dotCount = c == '.' ? dotCount + 1 : 0;
-        prevChar = c;
-      }
-      if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
-        normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
-      }
-      return normalizationLevel;
-    }
-
-    @Override
-    public String normalize(String path, int normalizationLevel) {
-      if (normalizationLevel == NORMALIZED) {
-        return path;
-      }
-      if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) {
-        String resolvedPath = shortPathResolver.resolveShortPath(path);
-        if (resolvedPath != null) {
-          path = resolvedPath;
-        }
-      }
-      String[] segments = path.split("[\\\\/]+");
-      int driveStrLength = getDriveStrLength(path);
-      boolean isAbsolute = driveStrLength > 0;
-      int segmentSkipCount = isAbsolute ? 1 : 0;
-
-      StringBuilder sb = new StringBuilder(path.length());
-      if (isAbsolute) {
-        char driveLetter = path.charAt(0);
-        sb.append(Character.toUpperCase(driveLetter));
-        sb.append(":/");
-      }
-      // unix path support
-      if (!path.isEmpty() && path.charAt(0) == '/') {
-        if (path.length() == 2 || (path.length() > 2 && path.charAt(2) == '/')) {
-          sb.append(Character.toUpperCase(path.charAt(1)));
-          sb.append(":/");
-          segmentSkipCount = 2;
-        } else {
-          String unixRoot = getUnixRoot();
-          sb.append(unixRoot);
-        }
-      }
-      int segmentCount = removeRelativePaths(segments, segmentSkipCount);
-      for (int i = 0; i < segmentCount; ++i) {
-        sb.append(segments[i]);
-        sb.append('/');
-      }
-      if (segmentCount > 0) {
-        sb.deleteCharAt(sb.length() - 1);
-      }
-      return sb.toString();
-    }
-
-    @Override
-    public int getDriveStrLength(String path) {
-      int n = path.length();
-      if (n < 3) {
-        return 0;
-      }
-      if (isDriveLetter(path.charAt(0))
-          && path.charAt(1) == ':'
-          && (path.charAt(2) == '/' || path.charAt(2) == '\\')) {
-        return 3;
-      }
-      return 0;
-    }
-
-    private static boolean isDriveLetter(char c) {
-      return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
-    }
-
-    @Override
-    public int compare(String s1, String s2) {
-      // Windows is case-insensitive
-      return s1.compareToIgnoreCase(s2);
-    }
-
-    @Override
-    public int hashPath(String s) {
-      // Windows is case-insensitive
-      return s.toLowerCase().hashCode();
-    }
-
-    @Override
-    public boolean startsWith(String path, String prefix) {
-      int pathn = path.length();
-      int prefixn = prefix.length();
-      if (pathn < prefixn) {
-        return false;
-      }
-      for (int i = 0; i < prefixn; ++i) {
-        if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    @Override
-    public char getSeparator() {
-      return '/';
-    }
-
-    @Override
-    public boolean isCaseSensitive() {
-      return false;
-    }
-
-    private String getUnixRoot() {
-      String value = UNIX_ROOT.get();
-      if (value == null) {
-        String jvmFlag = "bazel.windows_unix_root";
-        value = determineUnixRoot(jvmFlag);
-        if (value == null) {
-          throw new IllegalStateException(
-              String.format(
-                  "\"%1$s\" JVM flag is not set. Use the --host_jvm_args flag or export the "
-                      + "BAZEL_SH environment variable. For example "
-                      + "\"--host_jvm_args=-D%1$s=c:/tools/msys64\" or "
-                      + "\"set BAZEL_SH=c:/tools/msys64/usr/bin/bash.exe\".",
-                  jvmFlag));
-        }
-        if (getDriveStrLength(value) != 3) {
-          throw new IllegalStateException(
-              String.format("\"%s\" must be an absolute path, got: \"%s\"", jvmFlag, value));
-        }
-        value = value.replace('\\', '/');
-        if (value.length() > 3 && value.endsWith("/")) {
-          value = value.substring(0, value.length() - 1);
-        }
-        UNIX_ROOT.set(value);
-      }
-      return value;
-    }
-
-    private String determineUnixRoot(String jvmArgName) {
-      // Get the path from a JVM flag, if specified.
-      String path = System.getProperty(jvmArgName);
-      if (path == null) {
-        return null;
-      }
-      path = path.trim();
-      if (path.isEmpty()) {
-        return null;
-      }
-      return path;
-    }
-  }
-
-  private static OsPathPolicy createFilePathOs() {
-    switch (OS.getCurrent()) {
-      case LINUX:
-      case FREEBSD:
-      case UNKNOWN:
-        return new UnixOsPathPolicy();
-      case DARWIN:
-        return new MacOsPathPolicy();
-      case WINDOWS:
-        return new WindowsOsPathPolicy();
-      default:
-        throw new AssertionError("Not covering all OSs");
-    }
-  }
-
-  /**
-   * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the
-   * front. Returns the remaining number of items.
-   */
-  private static int removeRelativePaths(String[] segments, int starti) {
-    int segmentCount = 0;
-    int shift = starti;
-    for (int i = starti; i < segments.length; ++i) {
-      String segment = segments[i];
-      switch (segment) {
-        case ".":
-          // Just discard it
-          ++shift;
-          break;
-        case "..":
-          if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) {
-            // Remove the last segment, if there is one and it is not "..". This
-            // means that the resulting path can still contain ".."
-            // segments at the beginning.
-            segmentCount--;
-            shift += 2;
-            break;
-          }
-          // Fall through
-        default:
-          ++segmentCount;
-          if (shift > 0) {
-            segments[i - shift] = segments[i];
-          }
-          break;
-      }
-    }
-    return segmentCount;
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java
new file mode 100644
index 0000000..6b0380f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java
@@ -0,0 +1,144 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.vfs;
+
+import com.google.devtools.build.lib.util.OS;
+
+/**
+ * An interface class representing the differences in path style between different OSs.
+ *
+ * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc.
+ */
+public interface OsPathPolicy {
+  int NORMALIZED = 0; // Path is normalized
+  int NEEDS_NORMALIZE = 1; // Path requires normalization
+
+  /** Returns required normalization level, passed to {@link #normalize}. */
+  int needsToNormalize(String path);
+
+  /**
+   * Returns the required normalization level if an already normalized string is concatenated with
+   * another normalized path fragment.
+   *
+   * <p>This method may be faster than {@link #needsToNormalize(String)}.
+   */
+  int needsToNormalizeSuffix(String normalizedSuffix);
+
+  /**
+   * Normalizes the passed string according to the passed normalization level.
+   *
+   * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize}
+   */
+  String normalize(String path, int normalizationLevel);
+
+  /**
+   * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'.
+   *
+   * <p>If the path is relative, 0 is returned
+   */
+  int getDriveStrLength(String path);
+
+  /** Compares two path strings, using the given OS case sensitivity. */
+  int compare(String s1, String s2);
+
+  /** Compares two characters, using the given OS case sensitivity. */
+  int compare(char c1, char c2);
+
+  /** Tests two path strings for equality, using the given OS case sensitivity. */
+  boolean equals(String s1, String s2);
+
+  /** Computes the hash code for a path string. */
+  int hash(String s);
+
+  /**
+   * Returns whether the passed string starts with the given prefix, given the OS case sensitivity.
+   *
+   * <p>This is a pure string operation and doesn't need to worry about matching path segments.
+   */
+  boolean startsWith(String path, String prefix);
+
+  /**
+   * Returns whether the passed string ends with the given prefix, given the OS case sensitivity.
+   *
+   * <p>This is a pure string operation and doesn't need to worry about matching path segments.
+   */
+  boolean endsWith(String path, String suffix);
+
+  /** Returns the separator used for normalized paths. */
+  char getSeparator();
+
+  /** Returns whether the unnormalized character c is a separator. */
+  boolean isSeparator(char c);
+
+  boolean isCaseSensitive();
+
+  static OsPathPolicy getFilePathOs() {
+    switch (OS.getCurrent()) {
+      case LINUX:
+      case FREEBSD:
+      case UNKNOWN:
+        return UnixOsPathPolicy.INSTANCE;
+      case DARWIN:
+        // NOTE: We *should* return a path policy that is case insensitive,
+        // but we currently don't handle this
+        return UnixOsPathPolicy.INSTANCE;
+      case WINDOWS:
+        return WindowsOsPathPolicy.INSTANCE;
+      default:
+        throw new AssertionError("Not covering all OSs");
+    }
+  }
+
+  /** Utilities for implementations of {@link OsPathPolicy}. */
+  class Utils {
+    /**
+     * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the
+     * front. Returns the remaining number of items.
+     */
+    static int removeRelativePaths(String[] segments, int starti, boolean isAbsolute) {
+      int segmentCount = 0;
+      int shift = starti;
+      int n = segments.length;
+      for (int i = starti; i < n; ++i) {
+        String segment = segments[i];
+        switch (segment) {
+          case ".":
+            ++shift;
+            break;
+          case "..":
+            if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) {
+              // Remove the last segment, if there is one and it is not "..". This
+              // means that the resulting path can still contain ".."
+              // segments at the beginning.
+              segmentCount--;
+              shift += 2;
+              break;
+            } else if (isAbsolute) {
+              // If this is absolute, then just pop it the ".." off and remain at root
+              ++shift;
+              break;
+            }
+            // Fall through
+          default:
+            ++segmentCount;
+            if (shift > 0) {
+              segments[i - shift] = segments[i];
+            }
+            break;
+        }
+      }
+      return segmentCount;
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/Path.java b/src/main/java/com/google/devtools/build/lib/vfs/Path.java
index 9549d25..075c47b 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
@@ -14,14 +14,14 @@
 package com.google.devtools.build.lib.vfs;
 
 import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodec;
+import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
 import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
+import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.util.FileType;
-import com.google.devtools.build.lib.util.StringCanonicalizer;
 import com.google.devtools.build.lib.vfs.FileSystem.HashFunction;
 import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.CodedOutputStream;
@@ -33,28 +33,29 @@
 import java.io.ObjectOutputStream;
 import java.io.OutputStream;
 import java.io.Serializable;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.IdentityHashMap;
-import java.util.Objects;
+import javax.annotation.Nullable;
 
 /**
- * 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.
+ * A local file path representing a file on the host machine. You should use this when you want to
+ * access local files via the file system.
+ *
+ * <p>Paths are always absolute.
+ *
+ * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
+ * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased.
+ * The current implementation does not touch the incoming path string unless the string actually
+ * needs to be normalized.
  *
  * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
- * front of a path (c:/abc) are supported. However, Windows-style backslash separators
- * (C:\\foo\\bar) and drive-relative paths ("C:foo") are explicitly not supported, same with
- * advanced features like \\\\network\\paths and \\\\?\\unc\\paths.
+ * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as
+ * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like
+ * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
+ * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
+ * normalization.
  *
- * <p>{@link FileSystem} implementations maintain pointers into this graph.
+ * <p>Mac and Windows file paths are case insensitive. Case is preserved.
  */
 @ThreadSafe
 public class Path
@@ -62,34 +63,6 @@
   public static final InjectingObjectCodec<Path, FileSystemProvider> CODEC =
       new PathCodecWithInjectedFileSystem();
 
-  /** Filesystem-specific factory for {@link Path} objects. */
-  public static interface PathFactory {
-    /**
-     * Creates the root of all paths used by a filesystem.
-     *
-     * <p>All other paths are instantiated via {@link Path#createChildPath(String)} which calls
-     * {@link #createChildPath(Path, String)}.
-     *
-     * <p>Beware: this is called during the FileSystem constructor which may occur before subclasses
-     * are completely initialized.
-     */
-    Path createRootPath(FileSystem filesystem);
-
-    /**
-     * Create a child path of the given parent.
-     *
-     * <p>All {@link Path} objects are instantiated via this method, with the sole exception of the
-     * filesystem root, which is created by {@link #createRootPath(FileSystem)}.
-     */
-    Path createChildPath(Path parent, String childName);
-
-    /**
-     * Makes the proper invocation of {@link FileSystem#getCachedChildPathInternal}, doing
-     * filesystem-specific logic if necessary.
-     */
-    Path getCachedChildPathInternal(Path path, String childName);
-  }
-
   private static FileSystem fileSystemForSerialization;
 
   /**
@@ -106,398 +79,279 @@
     return fileSystemForSerialization;
   }
 
-  // These are basically final, but can't be marked as such in order to support serialization.
+  private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
+  private static final char SEPARATOR = OS.getSeparator();
+
+  private String path;
+  private int driveStrLength; // 1 on Unix, 3 on Windows
   private FileSystem fileSystem;
-  private String name;
-  private Path parent;
-  private int depth;
-  private int hashCode;
 
-  private static final ReferenceQueue<Path> REFERENCE_QUEUE = new ReferenceQueue<>();
-
-  private static class PathWeakReferenceForCleanup extends WeakReference<Path> {
-    final Path parent;
-    final String baseName;
-
-    PathWeakReferenceForCleanup(Path referent, ReferenceQueue<Path> referenceQueue) {
-      super(referent, referenceQueue);
-      parent = referent.getParentDirectory();
-      baseName = referent.getBaseName();
-    }
+  /** Creates a local path that is specific to the host OS. */
+  static Path create(String path, FileSystem fileSystem) {
+    Preconditions.checkNotNull(path);
+    int normalizationLevel = OS.needsToNormalize(path);
+    String normalizedPath = OS.normalize(path, normalizationLevel);
+    return createAlreadyNormalized(normalizedPath, fileSystem);
   }
 
-  private static final Thread PATH_CHILD_CACHE_CLEANUP_THREAD = new Thread("Path cache cleanup") {
-    @Override
-    public void run() {
-      while (true) {
-        try {
-          PathWeakReferenceForCleanup ref = (PathWeakReferenceForCleanup) REFERENCE_QUEUE.remove();
-          Path parent = ref.parent;
-          synchronized (parent) {
-            // It's possible that since this reference was enqueued for deletion, the Path was
-            // recreated with a new entry in the map. We definitely shouldn't delete that entry, so
-            // check to make sure they're the same.
-            Reference<Path> currentRef = ref.parent.children.get(ref.baseName);
-            if (currentRef == ref) {
-              ref.parent.children.remove(ref.baseName);
-            }
-          }
-        } catch (InterruptedException e) {
-          // Ignored.
-        }
-      }
-    }
-  };
-
-  static {
-    PATH_CHILD_CACHE_CLEANUP_THREAD.setDaemon(true);
-    PATH_CHILD_CACHE_CLEANUP_THREAD.start();
+  static Path createAlreadyNormalized(String path, FileSystem fileSystem) {
+    int driveStrLength = OS.getDriveStrLength(path);
+    return createAlreadyNormalized(path, driveStrLength, fileSystem);
   }
 
-  /**
-   * A mapping from a child file name to the {@link Path} representing it.
-   *
-   * <p>File names must be a single path segment.  The strings must be
-   * canonical.  We use IdentityHashMap instead of HashMap for reasons of space
-   * efficiency: instances are smaller by a single word.  Also, since all path
-   * segments are interned, the universe of Paths holds a minimal number of
-   * references to strings.  (It's doubtful that there's any time gain from use
-   * of an IdentityHashMap, since the time saved by avoiding string equality
-   * tests during hash lookups is probably equal to the time spent eagerly
-   * interning strings, unless the collision rate is high.)
-   *
-   * <p>The Paths are stored as weak references to ensure that a live
-   * Path for a directory does not hold a strong reference to all of its
-   * descendants, which would prevent collection of paths we never intend to
-   * use again.  Stale references in the map must be treated as absent.
-   *
-   * <p>A Path may be recycled once there is no Path that refers to it or
-   * to one of its descendants.  This means that any data stored in the
-   * Path instance by one of its subclasses must be recoverable: it's ok to
-   * store data in Paths as an optimization, but there must be another
-   * source for that data in case the Path is recycled.
-   *
-   * <p>We intentionally avoid using the existing library classes for reasons of
-   * space efficiency: while ConcurrentHashMap would reduce our locking
-   * overhead, and ReferenceMap would simplify the code a little, both of those
-   * classes have much higher per-instance overheads than IdentityHashMap.
-   *
-   * <p>The Path object must be synchronized while children is being
-   * accessed.
-   */
-  private volatile IdentityHashMap<String, Reference<Path>> children;
+  static Path createAlreadyNormalized(String path, int driveStrLength, FileSystem fileSystem) {
+    return new Path(path, driveStrLength, fileSystem);
+  }
 
-  /**
-   * Create a path instance.
-   *
-   * <p>Should only be called by {@link PathFactory#createChildPath(Path, String)}.
-   *
-   * @param name the name of this path; it must be canonicalized with {@link
-   *     StringCanonicalizer#intern}
-   * @param parent this path's parent
-   */
-  protected Path(FileSystem fileSystem, String name, Path parent) {
+  /** This method expects path to already be normalized. */
+  private Path(String path, int driveStrLength, FileSystem fileSystem) {
+    Preconditions.checkArgument(driveStrLength > 0, "Paths must be absolute: '%s'", path);
+    this.path = Preconditions.checkNotNull(path);
+    this.driveStrLength = driveStrLength;
     this.fileSystem = fileSystem;
-    this.name = name;
-    this.parent = parent;
-    this.depth = parent == null ? 0 : parent.depth + 1;
-
-    // No need to include the drive letter in the hash code, because it's derived from the parent
-    // and/or the name.
-    if (fileSystem == null || fileSystem.isFilePathCaseSensitive()) {
-      this.hashCode = Objects.hash(parent, name);
-    } else {
-      this.hashCode = Objects.hash(parent, name.toLowerCase());
-    }
   }
 
-  /**
-   * Create the root path.
-   *
-   * <p>Should only be called by {@link PathFactory#createRootPath(FileSystem)}.
-   */
-  protected Path(FileSystem fileSystem) {
-    this(fileSystem, StringCanonicalizer.intern("/"), null);
-  }
-
-  private void writeObject(ObjectOutputStream out) throws IOException {
-    Preconditions.checkState(
-        fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization);
-    out.writeUTF(getPathString());
-  }
-
-  private void readObject(ObjectInputStream in) throws IOException {
-    fileSystem = fileSystemForSerialization;
-    String p = in.readUTF();
-    PathFragment pf = PathFragment.create(p);
-    PathFragment parentDir = pf.getParentDirectory();
-    if (parentDir == null) {
-      this.name = "/";
-      this.parent = null;
-      this.depth = 0;
-    } else {
-      this.name = pf.getBaseName();
-      this.parent = fileSystem.getPath(parentDir);
-      this.depth = this.parent.depth + 1;
-    }
-    this.hashCode = Objects.hash(parent, name);
-    reinitializeAfterDeserialization();
-  }
-
-  /**
-   * Returns the filesystem instance to which this path belongs.
-   */
-  public FileSystem getFileSystem() {
-    return fileSystem;
-  }
-
-  public boolean isRootDirectory() {
-    return parent == null;
-  }
-
-  protected Path createChildPath(String childName) {
-    return fileSystem.getPathFactory().createChildPath(this, childName);
-  }
-
-  /**
-   * Reinitializes this object after deserialization.
-   *
-   * <p>Derived classes should use this hook to initialize additional state.
-   */
-  protected void reinitializeAfterDeserialization() {}
-
-  /**
-   * Returns true if {@code ancestorPath} may be an ancestor of {@code path}.
-   *
-   * <p>The return value may be a false positive, but it cannot be a false negative. This means that
-   * a true return value doesn't mean the ancestor candidate is really an ancestor, however a false
-   * return value means it's guaranteed that {@code ancestorCandidate} is not an ancestor of this
-   * path.
-   *
-   * <p>Subclasses may override this method with filesystem-specific logic, e.g. a Windows
-   * filesystem may return false if the ancestor path is on a different drive than this one, because
-   * it is then guaranteed that the ancestor candidate cannot be an ancestor of this path.
-   *
-   * @param ancestorCandidate the path that may or may not be an ancestor of this one
-   */
-  protected boolean isMaybeRelativeTo(Path ancestorCandidate) {
-    return true;
-  }
-
-  /**
-   * Returns true if this directory is top-level, i.e. it is its own parent.
-   *
-   * <p>When canonicalizing paths the ".." segment of a top-level directory always resolves to the
-   * directory itself.
-   *
-   * <p>On Unix, a top-level directory would be just the filesystem root ("/), on Windows it would
-   * be the filesystem root and the volume roots.
-   */
-  protected boolean isTopLevelDirectory() {
-    return isRootDirectory();
-  }
-
-  /**
-   * Returns the child path named name, or creates such a path (and caches it)
-   * if it doesn't already exist.
-   */
-  private Path getCachedChildPath(String childName) {
-    return fileSystem.getPathFactory().getCachedChildPathInternal(this, childName);
-  }
-
-  /**
-   * Internal method only intended to be called by {@link PathFactory#getCachedChildPathInternal}.
-   */
-  public static Path getCachedChildPathInternal(Path parent, String childName, boolean cacheable) {
-    // We get a canonical instance since 'children' is an IdentityHashMap.
-    childName = StringCanonicalizer.intern(childName);
-    if (!cacheable) {
-      // Non-cacheable children won't show up in `children` so applyToChildren won't run for these.
-      return parent.createChildPath(childName);
-    }
-
-    synchronized (parent) {
-      if (parent.children == null) {
-        // 66% of Paths have size == 1, 80% <= 2
-        parent.children = new IdentityHashMap<>(1);
-      }
-      Reference<Path> childRef = parent.children.get(childName);
-      Path child;
-      if (childRef == null || (child = childRef.get()) == null) {
-        child = parent.createChildPath(childName);
-        parent.children.put(childName, new PathWeakReferenceForCleanup(child, REFERENCE_QUEUE));
-      }
-      return child;
-    }
-  }
-
-  /**
-   * Applies the specified function to each {@link Path} that is an existing direct
-   * descendant of this one.  The Predicate is evaluated only for its
-   * side-effects.
-   *
-   * <p>This function exists to hide the "children" field, whose complex
-   * synchronization and identity requirements are too unsafe to be exposed to
-   * subclasses.  For example, the "children" field must be synchronized for
-   * the duration of any iteration over it; it may be null; and references
-   * within it may be stale, and must be ignored.
-   */
-  protected synchronized void applyToChildren(Predicate<Path> function) {
-    if (children != null) {
-      for (Reference<Path> childRef : children.values()) {
-        Path child = childRef.get();
-        if (child != null) {
-          function.apply(child);
-        }
-      }
-    }
-  }
-
-  /**
-   * Returns whether this path is recursively "under" {@code prefix} - that is,
-   * whether {@code path} is a prefix of this path.
-   *
-   * <p>This method returns {@code true} when called with this path itself. This
-   * method acts independently of the existence of files or folders.
-   *
-   * @param prefix a path which may or may not be a prefix of this path
-   */
-  public boolean startsWith(Path prefix) {
-    Path n = this;
-    for (int i = 0, len = depth - prefix.depth; i < len; i++) {
-      n = n.getParentDirectory();
-    }
-    return prefix.equals(n);
-  }
-
-  /**
-   * Computes a string representation of this path, and writes it to the given string builder. Only
-   * called locally with a new instance.
-   */
-  protected void buildPathString(StringBuilder result) {
-    if (isRootDirectory()) {
-      result.append(PathFragment.ROOT_DIR);
-    } else {
-      parent.buildPathString(result);
-      if (!parent.isRootDirectory()) {
-        result.append(PathFragment.SEPARATOR_CHAR);
-      }
-      result.append(name);
-    }
-  }
-
-  /**
-   * Returns the path encoded as an {@link URI}.
-   *
-   * <p>This concrete implementation returns URIs with "file" as the scheme.
-   * For Example:
-   *  - On Unix the path "/tmp/foo bar.txt" will be encoded as
-   *    "file:///tmp/foo%20bar.txt".
-   *  - On Windows the path "C:\Temp\Foo Bar.txt" will be encoded as
-   *    "file:///C:/Temp/Foo%20Bar.txt"
-   *
-   * <p>Implementors extending this class for special filesystems will likely need to override
-   * this method.
-   *
-   * @throws URISyntaxException if the URI cannot be constructed.
-   */
-  public URI toURI() {
-    String ps = getPathString();
-    if (!ps.startsWith("/")) {
-      // On Windows URI's need to start with a '/'. i.e. C:\Foo\Bar would be file:///C:/Foo/Bar
-      ps = "/" + ps;
-    }
-    try {
-      return new URI("file",
-          // Needs to be "" instead of null, so that toString() will append "//" after the scheme.
-          // We need this for backwards compatibility reasons as some consumers of the BEP are
-          // broken.
-          "",
-          ps, null, null);
-    } catch (URISyntaxException e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
-  /**
-   * Returns the path as a string.
-   */
   public String getPathString() {
-    // Profile driven optimization:
-    // Preallocate a size determined by the depth, so that
-    // we do not have to expand the capacity of the StringBuilder
-    StringBuilder builder = new StringBuilder(depth * 20);
-    buildPathString(builder);
-    return builder.toString();
-  }
-
-  @Override
-  public void repr(SkylarkPrinter printer) {
-    printer.append(getPathString());
+    return path;
   }
 
   @Override
   public String filePathForFileTypeMatcher() {
-    return name;
+    return path;
   }
 
   /**
-   * Returns the path as a string.
+   * Returns the name of the leaf file or directory.
+   *
+   * <p>If called on a {@link Path} instance for a mount name (eg. '/' or 'C:/'), the empty string
+   * is returned.
    */
+  public String getBaseName() {
+    int lastSeparator = path.lastIndexOf(SEPARATOR);
+    return lastSeparator < driveStrLength
+        ? path.substring(driveStrLength)
+        : path.substring(lastSeparator + 1);
+  }
+
+  /** Synonymous with {@link Path#getRelative(String)}. */
+  public Path getChild(String child) {
+    FileSystemUtils.checkBaseName(child);
+    return getRelative(child);
+  }
+
+  /**
+   * Returns a {@link Path} instance representing the relative path between this {@link Path} and
+   * the given path.
+   */
+  public Path getRelative(PathFragment other) {
+    Preconditions.checkNotNull(other);
+    String otherStr = other.getPathString();
+    // Fast-path: The path fragment is already normal, use cheaper normalization check
+    return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr));
+  }
+
+  /**
+   * Returns a {@link Path} instance representing the relative path between this {@link Path} and
+   * the given path.
+   */
+  public Path getRelative(String other) {
+    Preconditions.checkNotNull(other);
+    return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other));
+  }
+
+  private Path getRelative(String other, int otherDriveStrLength, int normalizationLevel) {
+    if (other.isEmpty()) {
+      return this;
+    }
+    // This is an absolute path, simply return it
+    if (otherDriveStrLength > 0) {
+      String normalizedPath = OS.normalize(other, normalizationLevel);
+      return new Path(normalizedPath, otherDriveStrLength, fileSystem);
+    }
+    String newPath;
+    if (path.length() == driveStrLength) {
+      newPath = path + other;
+    } else {
+      newPath = path + '/' + other;
+    }
+    // Note that even if other came from a PathFragment instance we still might
+    // need to normalize the result if (for instance) other is a path that
+    // starts with '..'
+    newPath = OS.normalize(newPath, normalizationLevel);
+    return new Path(newPath, driveStrLength, fileSystem);
+  }
+
+  /**
+   * Returns the parent directory of this {@link Path}.
+   *
+   * <p>If called on a root (like '/'), it returns null.
+   */
+  @Nullable
+  public Path getParentDirectory() {
+    int lastSeparator = path.lastIndexOf(SEPARATOR);
+    if (lastSeparator < driveStrLength) {
+      if (path.length() > driveStrLength) {
+        String newPath = path.substring(0, driveStrLength);
+        return new Path(newPath, driveStrLength, fileSystem);
+      } else {
+        return null;
+      }
+    }
+    String newPath = path.substring(0, lastSeparator);
+    return new Path(newPath, driveStrLength, fileSystem);
+  }
+
+  /**
+   * Returns the drive.
+   *
+   * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/".
+   */
+  public String getDriveStr() {
+    return path.substring(0, driveStrLength);
+  }
+
+  /**
+   * Returns the {@link Path} relative to the base {@link Path}.
+   *
+   * <p>For example, <code>Path.create("foo/bar/wiz").relativeTo(Path.create("foo"))
+   * </code> returns <code>Path.create("bar/wiz")</code>.
+   *
+   * <p>If the {@link Path} is not a child of the passed {@link Path} an {@link
+   * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
+   * Path} instances aren't both absolute or both relative.
+   */
+  public PathFragment relativeTo(Path base) {
+    Preconditions.checkNotNull(base);
+    checkSameFileSystem(base);
+    String basePath = base.path;
+    if (!OS.startsWith(path, basePath)) {
+      throw new IllegalArgumentException(
+          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+    }
+    int bn = basePath.length();
+    if (bn == 0) {
+      return PathFragment.createAlreadyNormalized(path, driveStrLength);
+    }
+    if (path.length() == bn) {
+      return PathFragment.EMPTY_FRAGMENT;
+    }
+    final int lastSlashIndex;
+    if (basePath.charAt(bn - 1) == '/') {
+      lastSlashIndex = bn - 1;
+    } else {
+      lastSlashIndex = bn;
+    }
+    if (path.charAt(lastSlashIndex) != '/') {
+      throw new IllegalArgumentException(
+          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+    }
+    String newPath = path.substring(lastSlashIndex + 1);
+    return PathFragment.createAlreadyNormalized(newPath, 0);
+  }
+
+  /**
+   * Returns whether this path is an ancestor of another path.
+   *
+   * <p>A path is considered an ancestor of itself.
+   */
+  public boolean startsWith(Path other) {
+    if (fileSystem != other.fileSystem) {
+      return false;
+    }
+    return startsWith(other.path, other.driveStrLength);
+  }
+
+  /**
+   * Returns whether this path is an ancestor of another path.
+   *
+   * <p>A path is considered an ancestor of itself.
+   *
+   * <p>An absolute path can never be an ancestor of a relative path fragment.
+   */
+  public boolean startsWith(PathFragment other) {
+    if (!other.isAbsolute()) {
+      return false;
+    }
+    String otherPath = other.getPathString();
+    return startsWith(otherPath, OS.getDriveStrLength(otherPath));
+  }
+
+  private boolean startsWith(String otherPath, int otherDriveStrLength) {
+    Preconditions.checkNotNull(otherPath);
+    if (otherPath.length() > path.length()) {
+      return false;
+    }
+    if (driveStrLength != otherDriveStrLength) {
+      return false;
+    }
+    if (!OS.startsWith(path, otherPath)) {
+      return false;
+    }
+    return path.length() == otherPath.length() // Handle equal paths
+        || otherPath.length() == driveStrLength // Handle (eg.) 'C:/foo' starts with 'C:/'
+        // Handle 'true' ancestors, eg. "foo/bar" starts with "foo", but does not start with "fo"
+        || path.charAt(otherPath.length()) == SEPARATOR;
+  }
+
+  public FileSystem getFileSystem() {
+    return fileSystem;
+  }
+
+  public PathFragment asFragment() {
+    return PathFragment.createAlreadyNormalized(path, driveStrLength);
+  }
+
   @Override
   public String toString() {
-    return getPathString();
+    return path;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Path other = (Path) o;
+    if (fileSystem != other.fileSystem) {
+      return false;
+    }
+    return OS.equals(this.path, other.path);
   }
 
   @Override
   public int hashCode() {
-    return hashCode;
+    // Do not include file system for efficiency.
+    // In practice we never construct paths from different file systems.
+    return OS.hash(this.path);
   }
 
   @Override
-  public boolean equals(Object other) {
-    if (this == other) {
-      return true;
+  public int compareTo(Path o) {
+    // If they are on different file systems, the file system decides the ordering.
+    FileSystem otherFs = o.getFileSystem();
+    if (!fileSystem.equals(otherFs)) {
+      int thisFileSystemHash = System.identityHashCode(fileSystem);
+      int otherFileSystemHash = System.identityHashCode(otherFs);
+      if (thisFileSystemHash < otherFileSystemHash) {
+        return -1;
+      } else if (thisFileSystemHash > otherFileSystemHash) {
+        return 1;
+      }
     }
-    if (!(other instanceof Path)) {
-      return false;
-    }
-
-    Path otherPath = (Path) other;
-    if (hashCode != otherPath.hashCode) {
-      return false;
-    }
-
-    if (!fileSystem.equals(otherPath.fileSystem)) {
-      return false;
-    }
-
-    if (fileSystem.isFilePathCaseSensitive()) {
-      return name.equals(otherPath.name)
-          && Objects.equals(parent, otherPath.parent);
-    } else {
-      return name.toLowerCase().equals(otherPath.name.toLowerCase())
-          && Objects.equals(parent, otherPath.parent);
-    }
+    return OS.compare(this.path, o.path);
   }
 
-  /**
-   * Returns a string of debugging information associated with this path.
-   * The results are unspecified and MUST NOT be interpreted programmatically.
-   */
-  protected String toDebugString() {
-    return "";
+  @Override
+  public void repr(SkylarkPrinter printer) {
+    printer.append(path);
   }
 
-  /**
-   * Returns a path representing the parent directory of this path,
-   * or null iff this Path represents the root of the filesystem.
-   *
-   * <p>Note: This method normalises ".."  and "." path segments by string
-   * processing, not by directory lookups.
-   */
-  public Path getParentDirectory() {
-    return parent;
+  @Override
+  public void str(SkylarkPrinter printer) {
+    repr(printer);
   }
 
   /** Returns true iff this path denotes an existing file of any kind. Follows symbolic links. */
@@ -662,157 +516,6 @@
   }
 
   /**
-   * Returns the last segment of this path, or "/" for the root directory.
-   */
-  public String getBaseName() {
-    return name;
-  }
-
-  /**
-   * Interprets the name of a path segment relative to the current path and
-   * returns the result.
-   *
-   * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
-   * validate the existence of any path, nor resolve symbolic links. If 'prefix'
-   * is not canonical, then a 'name' of '..' will be interpreted incorrectly.
-   *
-   * @precondition segment contains no slashes.
-   */
-  private Path getCanonicalPath(String segment) {
-    if (segment.equals(".") || segment.isEmpty()) {
-      return this; // that's a noop
-    } else if (segment.equals("..")) {
-      // top-level directory's parent is root, when canonicalising:
-      return isTopLevelDirectory() ? this : parent;
-    } else {
-      return getCachedChildPath(segment);
-    }
-  }
-
-  /**
-   * Returns the path formed by appending the single non-special segment
-   * "baseName" to this path.
-   *
-   * <p>You should almost always use {@link #getRelative} instead, which has
-   * the same performance characteristics if the given name is a valid base
-   * name, and which also works for '.', '..', and strings containing '/'.
-   *
-   * @throws IllegalArgumentException if {@code baseName} is not a valid base
-   *     name according to {@link FileSystemUtils#checkBaseName}
-   */
-  public Path getChild(String baseName) {
-    FileSystemUtils.checkBaseName(baseName);
-    return getCachedChildPath(baseName);
-  }
-
-  protected Path getRootForRelativePathComputation(PathFragment suffix) {
-    return suffix.isAbsolute() ? fileSystem.getRootDirectory() : this;
-  }
-
-  /**
-   * Returns the path formed by appending the relative or absolute path fragment
-   * {@code suffix} to this path.
-   *
-   * <p>If suffix is absolute, the current path will be ignored; otherwise, they
-   * will be combined. Up-level references ("..") cause the preceding path
-   * segment to be elided; this interpretation is only correct if the base path
-   * is canonical.
-   */
-  public Path getRelative(PathFragment suffix) {
-    Path result = getRootForRelativePathComputation(suffix);
-    for (String segment : suffix.segments()) {
-      result = result.getCanonicalPath(segment);
-    }
-    return result;
-  }
-
-  /**
-   * Returns the path formed by appending the relative or absolute string
-   * {@code path} to this path.
-   *
-   * <p>If the given path string is absolute, the current path will be ignored;
-   * otherwise, they will be combined. Up-level references ("..") cause the
-   * preceding path segment to be elided.
-   *
-   * <p>This is a purely syntactic operation, i.e. it does no I/O, it does not
-   * validate the existence of any path, nor resolve symbolic links.
-   */
-  public Path getRelative(String path) {
-    // Fast path for valid base names.
-    if ((path.length() == 0) || (path.equals("."))) {
-      return this;
-    } else if (path.equals("..")) {
-      return isTopLevelDirectory() ? this : parent;
-    } else if (PathFragment.containsSeparator(path)) {
-      return getRelative(PathFragment.create(path));
-    } else {
-      return getCachedChildPath(path);
-    }
-  }
-
-  protected final String[] getSegments() {
-    String[] resultSegments = new String[depth];
-    Path currentPath = this;
-    for (int pos = depth - 1; pos >= 0; pos--) {
-      resultSegments[pos] = currentPath.getBaseName();
-      currentPath = currentPath.getParentDirectory();
-    }
-    return resultSegments;
-  }
-
-  /** Returns an absolute PathFragment representing this path. */
-  public PathFragment asFragment() {
-    return PathFragment.createAlreadyInterned('\0', true, getSegments());
-  }
-
-  /**
-   * Returns a relative path fragment to this path, relative to {@code
-   * ancestorDirectory}. {@code ancestorDirectory} must be on the same
-   * filesystem as this path. (Currently, both this path and "ancestorDirectory"
-   * must be absolute, though this restriction could be loosened.)
-   * <p>
-   * <code>x.relativeTo(z) == y</code> implies
-   * <code>z.getRelative(y.getPathString()) == x</code>.
-   * <p>
-   * For example, <code>"/foo/bar/wiz".relativeTo("/foo")</code> returns
-   * <code>"bar/wiz"</code>.
-   *
-   * @throws IllegalArgumentException if this path is not beneath {@code
-   *         ancestorDirectory} or if they are not part of the same filesystem
-   */
-  public PathFragment relativeTo(Path ancestorPath) {
-    checkSameFilesystem(ancestorPath);
-
-    if (isMaybeRelativeTo(ancestorPath)) {
-      // Fast path: when otherPath is the ancestor of this path
-      int resultSegmentCount = depth - ancestorPath.depth;
-      if (resultSegmentCount >= 0) {
-        String[] resultSegments = new String[resultSegmentCount];
-        Path currentPath = this;
-        for (int pos = resultSegmentCount - 1; pos >= 0; pos--) {
-          resultSegments[pos] = currentPath.getBaseName();
-          currentPath = currentPath.getParentDirectory();
-        }
-        if (ancestorPath.equals(currentPath)) {
-          return PathFragment.createAlreadyInterned('\0', false, resultSegments);
-        }
-      }
-    }
-
-    throw new IllegalArgumentException("Path " + this + " is not beneath " + ancestorPath);
-  }
-
-  /**
-   * Checks that "this" and "that" are paths on the same filesystem.
-   */
-  protected void checkSameFilesystem(Path that) {
-    if (this.fileSystem != that.fileSystem) {
-      throw new IllegalArgumentException("Files are on different filesystems: "
-          + this + ", " + that);
-    }
-  }
-
-  /**
    * Returns an output stream to the file denoted by the current path, creating it and truncating it
    * if necessary. The stream is opened for writing.
    *
@@ -868,7 +571,7 @@
    * @throws IOException if the creation of the symbolic link was unsuccessful for any reason
    */
   public void createSymbolicLink(Path target) throws IOException {
-    checkSameFilesystem(target);
+    checkSameFileSystem(target);
     fileSystem.createSymbolicLink(this, target.asFragment());
   }
 
@@ -943,7 +646,7 @@
    * @throws IOException if the rename failed for any reason
    */
   public void renameTo(Path target) throws IOException {
-    checkSameFilesystem(target);
+    checkSameFileSystem(target);
     fileSystem.renameTo(this, target);
   }
 
@@ -1183,69 +886,28 @@
     fileSystem.prefetchPackageAsync(this, maxDirs);
   }
 
-  /**
-   * Compare Paths of the same file system using their PathFragments.
-   *
-   * <p>Paths from different filesystems will be compared using the identity
-   * hash code of their respective filesystems.
-   */
-  @Override
-  public int compareTo(Path o) {
-    // Fast-path.
-    if (equals(o)) {
-      return 0;
+  private void checkSameFileSystem(Path that) {
+    if (this.fileSystem != that.fileSystem) {
+      throw new IllegalArgumentException(
+          "Files are on different filesystems: " + this + ", " + that);
     }
+  }
 
-    // If they are on different file systems, the file system decides the ordering.
-    FileSystem otherFs = o.getFileSystem();
-    if (!fileSystem.equals(otherFs)) {
-      int thisFileSystemHash = System.identityHashCode(fileSystem);
-      int otherFileSystemHash = System.identityHashCode(otherFs);
-      if (thisFileSystemHash < otherFileSystemHash) {
-        return -1;
-      } else if (thisFileSystemHash > otherFileSystemHash) {
-        return 1;
-      } else {
-        // TODO(bazel-team): Add a name to every file system to be used here.
-        return 0;
-      }
-    }
+  private void writeObject(ObjectOutputStream out) throws IOException {
+    Preconditions.checkState(
+        fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization);
+    out.writeUTF(path);
+  }
 
-    // Equal file system, but different paths, because of the canonicalization.
-    // We expect to often compare Paths that are very similar, for example for files in the same
-    // directory. This can be done efficiently by going up segment by segment until we get the
-    // identical path (canonicalization again), and then just compare the immediate child segments.
-    // Overall this is much faster than creating PathFragment instances, and comparing those, which
-    // requires us to always go up to the top-level directory and copy all segments into a new
-    // string array.
-    // This was previously showing up as a hotspot in a profile of globbing a large directory.
-    Path a = this;
-    Path b = o;
-    int maxDepth = Math.min(a.depth, b.depth);
-    while (a.depth > maxDepth) {
-      a = a.getParentDirectory();
-    }
-    while (b.depth > maxDepth) {
-      b = b.getParentDirectory();
-    }
-    // One is the child of the other.
-    if (a.equals(b)) {
-      // If a is the same as this, this.depth must be less than o.depth.
-      return equals(a) ? -1 : 1;
-    }
-    Path previousa;
-    Path previousb;
-    do {
-      previousa = a;
-      previousb = b;
-      a = a.getParentDirectory();
-      b = b.getParentDirectory();
-    } while (!a.equals(b)); // This has to happen eventually.
-    return previousa.name.compareTo(previousb.name);
+  private void readObject(ObjectInputStream in) throws IOException {
+    path = in.readUTF();
+    fileSystem = fileSystemForSerialization;
+    driveStrLength = OS.getDriveStrLength(path);
   }
 
   private static class PathCodecWithInjectedFileSystem
       implements InjectingObjectCodec<Path, FileSystemProvider> {
+    private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized();
 
     @Override
     public Class<Path> getEncodedClass() {
@@ -1256,14 +918,14 @@
     public void serialize(FileSystemProvider fsProvider, Path path, CodedOutputStream codedOut)
         throws IOException, SerializationException {
       Preconditions.checkArgument(path.getFileSystem() == fsProvider.getFileSystem());
-      PathFragment.CODEC.serialize(path.asFragment(), codedOut);
+      stringCodec.serialize(path.getPathString(), codedOut);
     }
 
     @Override
     public Path deserialize(FileSystemProvider fsProvider, CodedInputStream codedIn)
         throws IOException, SerializationException {
-      PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn);
-      return fsProvider.getFileSystem().getPath(pathFragment);
+      return Path.createAlreadyNormalized(
+          stringCodec.deserialize(codedIn), fsProvider.getFileSystem());
     }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
index ee0ef36..c7644ed 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathCodec.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
 import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
+import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs;
 import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.CodedOutputStream;
 import java.io.IOException;
@@ -24,6 +25,7 @@
 /** Custom serialization for {@link Path}s. */
 public class PathCodec implements ObjectCodec<Path> {
 
+  private final ObjectCodec<String> stringCodec = StringCodecs.asciiOptimized();
   private final FileSystem fileSystem;
 
   /** Create an instance for serializing and deserializing {@link Path}s on {@code fileSystem}. */
@@ -41,15 +43,15 @@
       throws IOException, SerializationException {
     Preconditions.checkState(
         path.getFileSystem() == fileSystem,
-        "Path's FileSystem (%s) did not match the configured FileSystem (%s)",
+        "Path's FileSystem (%s) did not match the configured FileSystem (%s) for path (%s)",
         path.getFileSystem(),
-        fileSystem);
-    PathFragment.CODEC.serialize(path.asFragment(), codedOut);
+        fileSystem,
+        path);
+    stringCodec.serialize(path.getPathString(), codedOut);
   }
 
   @Override
   public Path deserialize(CodedInputStream codedIn) throws IOException, SerializationException {
-    PathFragment pathFragment = PathFragment.CODEC.deserialize(codedIn);
-    return fileSystem.getPath(pathFragment);
+    return fileSystem.getPath(stringCodec.deserialize(codedIn));
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
index 572042a..88ae732 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
@@ -1,4 +1,4 @@
-// Copyright 2014 The Bazel Authors. All rights reserved.
+// Copyright 2017 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,288 +16,517 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.analysis.actions.CommandLineItem;
-import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
 import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
 import com.google.devtools.build.lib.skyframe.serialization.strings.StringCodecs;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.util.FileType;
-import com.google.devtools.build.lib.util.OS;
-import com.google.devtools.build.lib.util.StringCanonicalizer;
 import com.google.protobuf.CodedInputStream;
 import com.google.protobuf.CodedOutputStream;
-import java.io.File;
 import java.io.IOException;
-import java.io.InvalidObjectException;
-import java.io.ObjectInputStream;
 import java.io.Serializable;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Set;
+import javax.annotation.Nullable;
 
 /**
- * This class represents an immutable filesystem path, which may be absolute or relative. The path
- * is maintained as a simple ordered list of path segment strings.
+ * A path segment representing a path fragment using the host machine's path style. That is; If you
+ * are running on a Unix machine, the path style will be unix, on Windows it is the windows path
+ * style.
  *
- * <p>This class is independent from other VFS classes, especially anything requiring native code.
- * It is safe to use in places that need simple segmented string path functionality.
+ * <p>Path fragments are either absolute or relative.
+ *
+ * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
+ * slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased.
+ * The current implementation does not touch the incoming path string unless the string actually
+ * needs to be normalized.
  *
  * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
  * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as
  * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like
- * \\\\network\\paths and \\\\?\\unc\\paths are not supported.
+ * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
+ * slashes ('/') even on Windows.
+ *
+ * <p>Mac and Windows path fragments are case insensitive.
  */
-@Immutable
-@javax.annotation.concurrent.Immutable
-@ThreadSafe
-public abstract class PathFragment
+public final class PathFragment
     implements Comparable<PathFragment>,
         Serializable,
-        SkylarkPrintable,
         FileType.HasFileType,
+        SkylarkPrintable,
         CommandLineItem {
-  private static final Helper HELPER =
-      OS.getCurrent() == OS.WINDOWS ? WindowsPathFragment.HELPER : UnixPathFragment.HELPER;
+  private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
 
-  public static final char SEPARATOR_CHAR = HELPER.getPrimarySeparatorChar();
-
+  public static final PathFragment EMPTY_FRAGMENT = create("");
+  public static final char SEPARATOR_CHAR = OS.getSeparator();
   public static final int INVALID_SEGMENT = -1;
 
-  public static final String ROOT_DIR = "/";
-
-  /** An empty path fragment. */
-  public static final PathFragment EMPTY_FRAGMENT = create("");
-
-  /** The path fragment representing the root directory. */
-  public static final PathFragment ROOT_FRAGMENT = create(ROOT_DIR);
+  private final String path;
+  private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
 
   public static final ObjectCodec<PathFragment> CODEC = new PathFragmentCodec();
 
-  /**
-   * A helper object for manipulating the various internal {@link PathFragment} implementations.
-   *
-   * <p>There will be exactly one {@link Helper} instance used to manipulate all the {@link
-   * PathFragment} instances (see {@link PathFragment#HELPER}). All of the various {@link Helper}
-   * and {@link PathFragment} implementations may assume this property.
-   */
-  protected abstract static class Helper {
-    /**
-     * Returns whether the two given arrays of segments have the same length and should be
-     * considered have logically equal contents.
-     */
-    protected final boolean segmentsEqual(String[] segments1, String[] segments2) {
-      return segments1.length == segments2.length
-          && segmentsEqual(segments1.length, segments1, 0, segments2);
-    }
-
-    /**
-     * Returns whether the {@code length} segments in {@code segments1}, starting at {@code offset1}
-     * should be considered to be logically equal to the first {@code length} segments in {@code
-     * segments2}.
-     */
-    abstract boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2);
-
-    /** Returns the comparison result of two {@link PathFragment} instances. */
-    protected abstract int compare(PathFragment pathFragment1, PathFragment pathFragment2);
-
-    /** Returns a fresh {@link PathFragment} instance from the given path string. */
-    abstract PathFragment create(String path);
-    /**
-     * Returns a fresh {@link PathFragment} instance from the given information, taking ownership of
-     * {@code segments} and assuming the {@link String}s within have already been interned.
-     */
-    abstract PathFragment createAlreadyInterned(
-        char driveLetter, boolean isAbsolute, String[] segments);
-
-    /** Returns whether {@code c} is a path separator. */
-    abstract boolean isSeparator(char c);
-    /** Returns the primary path separator. */
-    abstract char getPrimarySeparatorChar();
-    /** Return whether the given {@code path} contains a path separator. */
-    abstract boolean containsSeparatorChar(String path);
-
-    /**
-     * Splits the given {@code toSegment} into path segments, starting at the given {@code offset}.
-     */
-    protected final String[] segment(String toSegment, int offset) {
-      int length = toSegment.length();
-
-      // We make two passes through the array of characters: count & alloc,
-      // because simply using ArrayList was a bottleneck showing up during profiling.
-      int seg = 0;
-      int start = offset;
-      for (int i = offset; i < length; i++) {
-        if (isSeparator(toSegment.charAt(i))) {
-          if (i > start) { // to skip repeated separators
-            seg++;
-          }
-          start = i + 1;
-        }
-      }
-      if (start < length) {
-        seg++;
-      }
-      String[] result = new String[seg];
-      seg = 0;
-      start = offset;
-      for (int i = offset; i < length; i++) {
-        if (isSeparator(toSegment.charAt(i))) {
-          if (i > start) { // to skip repeated separators
-            result[seg] = StringCanonicalizer.intern(toSegment.substring(start, i));
-            seg++;
-          }
-          start = i + 1;
-        }
-      }
-      if (start < length) {
-        result[seg] = StringCanonicalizer.intern(toSegment.substring(start, length));
-      }
-      return result;
-    }
-  }
-
-  /** Lower-level API. Create a PathFragment, interning segments. */
-  public static PathFragment create(char driveLetter, boolean isAbsolute, String[] segments) {
-    String[] internedSegments = new String[segments.length];
-    for (int i = 0; i < segments.length; i++) {
-      internedSegments[i] = StringCanonicalizer.intern(segments[i]);
-    }
-    return createAlreadyInterned(driveLetter, isAbsolute, internedSegments);
-  }
-
-  /** Same as {@link #create(char, boolean, String[])}, except for {@link List}s of segments. */
-  public static PathFragment create(char driveLetter, boolean isAbsolute, List<String> segments) {
-    String[] internedSegments = new String[segments.size()];
-    for (int i = 0; i < segments.size(); i++) {
-      internedSegments[i] = StringCanonicalizer.intern(segments.get(i));
-    }
-    return createAlreadyInterned(driveLetter, isAbsolute, internedSegments);
-  }
-
-  /**
-   * Construct a PathFragment from a java.io.File, which is an absolute or
-   * relative UNIX path.  Does not support Windows-style Files.
-   */
-  public static PathFragment create(File path) {
-    return HELPER.create(path.getPath());
-  }
-
-  /**
-   * Construct a PathFragment from a string, which is an absolute or relative UNIX or Windows path.
-   */
+  /** Creates a new normalized path fragment. */
   public static PathFragment create(String path) {
-    return HELPER.create(path);
+    int normalizationLevel = OS.needsToNormalize(path);
+    String normalizedPath =
+        normalizationLevel != OsPathPolicy.NORMALIZED
+            ? OS.normalize(path, normalizationLevel)
+            : path;
+    int driveStrLength = OS.getDriveStrLength(normalizedPath);
+    return new PathFragment(normalizedPath, driveStrLength);
   }
 
   /**
-   * Constructs a PathFragment, taking ownership of {@code segments} and assuming the {@link
-   * String}s within have already been interned.
+   * Creates a new path fragment, where the caller promises that the path is normalized.
    *
-   * <p>Package-private because it does not perform a defensive copy of the segments array. Used
-   * here in PathFragment, and by Path.asFragment() and Path.relativeTo().
+   * <p>WARNING! Make sure the path fragment is in fact already normalized. The rest of the code
+   * assumes this is the case.
    */
-  static PathFragment createAlreadyInterned(
-      char driveLetter, boolean isAbsolute, String[] segments) {
-    return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments);
-  }
-
-  /** Returns whether the current {@code path} contains a path separator. */
-  static boolean containsSeparator(String path) {
-    return HELPER.containsSeparatorChar(path);
+  public static PathFragment createAlreadyNormalized(String normalizedPath) {
+    int driveStrLength = OS.getDriveStrLength(normalizedPath);
+    return createAlreadyNormalized(normalizedPath, driveStrLength);
   }
 
   /**
-   * Construct a PathFragment from a sequence of other PathFragments. The new fragment will be
-   * absolute iff the first fragment was absolute.
+   * Creates a new path fragment, where the caller promises that the path is normalized.
+   *
+   * <p>Should only be used internally.
    */
-  // TODO(bazel-team): Most usages of this method are wasteful from a garbage perspective. Refactor
-  // to something better.
-  public static PathFragment create(PathFragment first, PathFragment second, PathFragment... more) {
-    String[] segments = new String[sumLengths(first, second, more)];
-    int offset = 0;
-    offset += addSegmentsTo(segments, offset, first);
-    offset += addSegmentsTo(segments, offset, second);
-    for (PathFragment fragment : more) {
-      offset += addSegmentsTo(segments, offset, fragment);
-    }
-    boolean isAbsolute = first.isAbsolute();
-    char driveLetter = first.getDriveLetter();
-    return HELPER.createAlreadyInterned(driveLetter, isAbsolute, segments);
+  static PathFragment createAlreadyNormalized(String normalizedPath, int driveStrLength) {
+    return new PathFragment(normalizedPath, driveStrLength);
   }
 
-  // Medium sized builds can easily hold millions of live PathFragments, so the per-instance size of
-  // PathFragment is a concern.
-  //
-  // We have two oop-sized fields (segments, path), and one 4-byte-sized one (hashCode).
-  //
-  // If Blaze is run on a jvm with -XX:+UseCompressedOops, each PathFragment instance is 24 bytes
-  // and so adding any additional field will increase the per-instance size to at least 32 bytes.
-  //
-  // If Blaze is run on a jvm with -XX:-UseCompressedOops, each PathFragment instance is 32 bytes
-  // and so adding any additional field will increase the per-instance size to at least 40 bytes.
-  //
-  // Therefore, do not add any additional fields unless you have considered the memory implications.
-
-  // The individual path components.
-  // Does *not* include the Windows drive letter.
-  protected final String[] segments;
-
-  // hashCode and path are lazily initialized but semantically immutable.
-  private int hashCode;
-  private String path;
-
-  protected PathFragment(String[] segments) {
-    this.segments = segments;
+  /** This method expects path to already be normalized. */
+  private PathFragment(String path, int driveStrLength) {
+    this.path = Preconditions.checkNotNull(path);
+    this.driveStrLength = driveStrLength;
   }
 
-  private static int addSegmentsTo(String[] segments, int offset, PathFragment fragment) {
-    int count = fragment.segmentCount();
-    System.arraycopy(fragment.segments, 0, segments, offset, count);
-    return count;
-  }
-
-  private static int sumLengths(PathFragment first, PathFragment second, PathFragment[] more) {
-    int total = first.segmentCount() + second.segmentCount();
-    for (PathFragment fragment : more) {
-      total += fragment.segmentCount();
-    }
-    return total;
-  }
-
-  protected Object writeReplace() {
-    return new PathFragmentSerializationProxy(toString());
-  }
-
-  protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
-    throw new InvalidObjectException("Serialization is allowed only by proxy");
-  }
-
-  /**
-   * Returns the path string using '/' as the name-separator character.  Returns "" if the path
-   * is both relative and empty.
-   */
   public String getPathString() {
-    // Double-checked locking works, even without volatile, because path is a String, according to:
-    // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
-    if (path == null) {
-      synchronized (this) {
-        if (path == null) {
-          path = StringCanonicalizer.intern(joinSegments(HELPER.getPrimarySeparatorChar()));
-        }
-      }
-    }
     return path;
   }
 
+  public boolean isEmpty() {
+    return path.isEmpty();
+  }
+
+  int getDriveStrLength() {
+    return driveStrLength;
+  }
+
   /**
-   * Returns "." if the path fragment is both relative and empty, or {@link
-   * #getPathString} otherwise.
+   * If called on a {@link PathFragment} instance for a mount name (eg. '/' or 'C:/'), the empty
+   * string is returned.
+   *
+   * <p>This operation allocates a new string.
    */
-  // TODO(bazel-team): Change getPathString to do this - this behavior makes more sense.
+  public String getBaseName() {
+    int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR);
+    return lastSeparator < driveStrLength
+        ? path.substring(driveStrLength)
+        : path.substring(lastSeparator + 1);
+  }
+
+  /**
+   * Returns a {@link PathFragment} instance representing the relative path between this {@link
+   * PathFragment} and the given {@link PathFragment}.
+   *
+   * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve
+   * symlinks.
+   */
+  public PathFragment getRelative(PathFragment other) {
+    Preconditions.checkNotNull(other);
+    // Fast-path: The path fragment is already normal, use cheaper normalization check
+    String otherStr = other.path;
+    return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr));
+  }
+
+  /**
+   * Returns a {@link PathFragment} instance representing the relative path between this {@link
+   * PathFragment} and the given path.
+   *
+   * <p>See {@link #getRelative(PathFragment)} for details.
+   */
+  public PathFragment getRelative(String other) {
+    Preconditions.checkNotNull(other);
+    return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other));
+  }
+
+  private PathFragment getRelative(String other, int otherDriveStrLength, int normalizationLevel) {
+    if (path.isEmpty()) {
+      return create(other);
+    }
+    if (other.isEmpty()) {
+      return this;
+    }
+    // This is an absolute path, simply return it
+    if (otherDriveStrLength > 0) {
+      String normalizedPath =
+          normalizationLevel != OsPathPolicy.NORMALIZED
+              ? OS.normalize(other, normalizationLevel)
+              : other;
+      return new PathFragment(normalizedPath, otherDriveStrLength);
+    }
+    String newPath;
+    if (path.length() == driveStrLength) {
+      newPath = path + other;
+    } else {
+      newPath = path + '/' + other;
+    }
+    newPath =
+        normalizationLevel != OsPathPolicy.NORMALIZED
+            ? OS.normalize(newPath, normalizationLevel)
+            : newPath;
+    return new PathFragment(newPath, driveStrLength);
+  }
+
+  public PathFragment getChild(String baseName) {
+    checkBaseName(baseName);
+    String newPath;
+    if (path.length() == driveStrLength) {
+      newPath = path + baseName;
+    } else {
+      newPath = path + '/' + baseName;
+    }
+    return new PathFragment(newPath, driveStrLength);
+  }
+
+  /**
+   * Returns the parent directory of this {@link PathFragment}.
+   *
+   * <p>If this is called on an single directory for a relative path, this returns an empty relative
+   * path. If it's called on a root (like '/') or the empty string, it returns null.
+   */
+  @Nullable
+  public PathFragment getParentDirectory() {
+    int lastSeparator = path.lastIndexOf(SEPARATOR_CHAR);
+
+    // For absolute paths we need to specially handle when we hit root
+    // Relative paths can't hit this path as driveStrLength == 0
+    if (driveStrLength > 0) {
+      if (lastSeparator < driveStrLength) {
+        if (path.length() > driveStrLength) {
+          String newPath = path.substring(0, driveStrLength);
+          return new PathFragment(newPath, driveStrLength);
+        } else {
+          return null;
+        }
+      }
+    } else {
+      if (lastSeparator == -1) {
+        if (!path.isEmpty()) {
+          return EMPTY_FRAGMENT;
+        } else {
+          return null;
+        }
+      }
+    }
+    String newPath = path.substring(0, lastSeparator);
+    return new PathFragment(newPath, driveStrLength);
+  }
+
+  /**
+   * Returns the {@link PathFragment} relative to the base {@link PathFragment}.
+   *
+   * <p>For example, <code>FilePath.create("foo/bar/wiz").relativeTo(FilePath.create("foo"))</code>
+   * returns <code>"bar/wiz"</code>.
+   *
+   * <p>If the {@link PathFragment} is not a child of the passed {@link PathFragment} an {@link
+   * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
+   * PathFragment} instances aren't both absolute or both relative.
+   */
+  public PathFragment relativeTo(PathFragment base) {
+    Preconditions.checkNotNull(base);
+    if (isAbsolute() != base.isAbsolute()) {
+      throw new IllegalArgumentException(
+          "Cannot relativize an absolute and a non-absolute path pair");
+    }
+    String basePath = base.path;
+    if (!OS.startsWith(path, basePath)) {
+      throw new IllegalArgumentException(
+          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+    }
+    int bn = basePath.length();
+    if (bn == 0) {
+      return this;
+    }
+    if (path.length() == bn) {
+      return EMPTY_FRAGMENT;
+    }
+    final int lastSlashIndex;
+    if (basePath.charAt(bn - 1) == '/') {
+      lastSlashIndex = bn - 1;
+    } else {
+      lastSlashIndex = bn;
+    }
+    if (path.charAt(lastSlashIndex) != '/') {
+      throw new IllegalArgumentException(
+          String.format("Path '%s' is not under '%s', cannot relativize", this, base));
+    }
+    String newPath = path.substring(lastSlashIndex + 1);
+    return new PathFragment(newPath, 0 /* Always a relative path */);
+  }
+
+  public PathFragment relativeTo(String base) {
+    return relativeTo(PathFragment.create(base));
+  }
+
+  /**
+   * Returns whether this path is an ancestor of another path.
+   *
+   * <p>If this == other, true is returned.
+   *
+   * <p>An absolute path can never be an ancestor of a relative path, and vice versa.
+   */
+  public boolean startsWith(PathFragment other) {
+    Preconditions.checkNotNull(other);
+    if (other.path.length() > path.length()) {
+      return false;
+    }
+    if (driveStrLength != other.driveStrLength) {
+      return false;
+    }
+    if (!OS.startsWith(path, other.path)) {
+      return false;
+    }
+    return path.length() == other.path.length()
+        || other.path.length() == driveStrLength
+        || path.charAt(other.path.length()) == SEPARATOR_CHAR;
+  }
+
+  /**
+   * Returns true iff {@code suffix}, considered as a list of path segments, is relative and a
+   * suffix of {@code this}, or both are absolute and equal.
+   *
+   * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial order)
+   */
+  public boolean endsWith(PathFragment other) {
+    Preconditions.checkNotNull(other);
+    if (other.path.length() > path.length()) {
+      return false;
+    }
+    if (other.isAbsolute()) {
+      return this.equals(other);
+    }
+    if (!OS.endsWith(path, other.path)) {
+      return false;
+    }
+    return path.length() == other.path.length()
+        || other.path.length() == 0
+        || path.charAt(path.length() - other.path.length() - 1) == SEPARATOR_CHAR;
+  }
+
+  public boolean isAbsolute() {
+    return driveStrLength > 0;
+  }
+
+  public static boolean isAbsolute(String path) {
+    return OS.getDriveStrLength(path) > 0;
+  }
+
+  @Override
+  public String toString() {
+    return path;
+  }
+
+  @Override
+  public void repr(SkylarkPrinter printer) {
+    printer.append(path);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    return OS.equals(this.path, ((PathFragment) o).path);
+  }
+
+  @Override
+  public int hashCode() {
+    return OS.hash(this.path);
+  }
+
+  @Override
+  public int compareTo(PathFragment o) {
+    return OS.compare(this.path, o.path);
+  }
+
+  ////////////////////////////////////////////////////////////////////////
+
+  /**
+   * Returns the number of segments in this path.
+   *
+   * <p>This operation is O(N) on the length of the string.
+   */
+  public int segmentCount() {
+    int n = path.length();
+    int segmentCount = 0;
+    int i;
+    for (i = driveStrLength; i < n; ++i) {
+      if (path.charAt(i) == SEPARATOR_CHAR) {
+        ++segmentCount;
+      }
+    }
+    // Add last segment if one exists.
+    if (i > driveStrLength) {
+      ++segmentCount;
+    }
+    return segmentCount;
+  }
+
+  /**
+   * Returns the specified segment of this path; index must be positive and less than numSegments().
+   *
+   * <p>This operation is O(N) on the length of the string.
+   */
+  public String getSegment(int index) {
+    int n = path.length();
+    int segmentCount = 0;
+    int i;
+    for (i = driveStrLength; i < n && segmentCount < index; ++i) {
+      if (path.charAt(i) == SEPARATOR_CHAR) {
+        ++segmentCount;
+      }
+    }
+    int starti = i;
+    for (; i < n; ++i) {
+      if (path.charAt(i) == SEPARATOR_CHAR) {
+        break;
+      }
+    }
+    // Add last segment if one exists.
+    if (i > driveStrLength) {
+      ++segmentCount;
+    }
+    int endi = i;
+    if (index < 0 || index >= segmentCount) {
+      throw new IllegalArgumentException("Illegal segment index: " + index);
+    }
+    return path.substring(starti, endi);
+  }
+
+  /**
+   * Returns a new path fragment that is a sub fragment of this one. The sub fragment begins at the
+   * specified <code>beginIndex</code> segment and ends at the segment at index <code>endIndex - 1
+   * </code>. Thus the number of segments in the new PathFragment is <code>endIndex - beginIndex
+   * </code>.
+   *
+   * <p>This operation is O(N) on the length of the string.
+   *
+   * @param beginIndex the beginning index, inclusive.
+   * @param endIndex the ending index, exclusive.
+   * @return the specified sub fragment, never null.
+   * @exception IndexOutOfBoundsException if the <code>beginIndex</code> is negative, or <code>
+   *     endIndex</code> is larger than the length of this <code>String</code> object, or <code>
+   *     beginIndex</code> is larger than <code>endIndex</code>.
+   */
+  public PathFragment subFragment(int beginIndex, int endIndex) {
+    if (beginIndex < 0 || beginIndex > endIndex) {
+      throw new IndexOutOfBoundsException(
+          String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex));
+    }
+    return subFragmentImpl(beginIndex, endIndex);
+  }
+
+  public PathFragment subFragment(int beginIndex) {
+    if (beginIndex < 0) {
+      throw new IndexOutOfBoundsException(
+          String.format("path: %s, beginIndex: %d", toString(), beginIndex));
+    }
+    return subFragmentImpl(beginIndex, -1);
+  }
+
+  private PathFragment subFragmentImpl(int beginIndex, int endIndex) {
+    int n = path.length();
+    int segmentIndex = 0;
+    int i;
+    for (i = driveStrLength; i < n && segmentIndex < beginIndex; ++i) {
+      if (path.charAt(i) == SEPARATOR_CHAR) {
+        ++segmentIndex;
+      }
+    }
+    int starti = i;
+    if (segmentIndex < endIndex) {
+      for (; i < n; ++i) {
+        if (path.charAt(i) == SEPARATOR_CHAR) {
+          ++segmentIndex;
+          if (segmentIndex == endIndex) {
+            break;
+          }
+        }
+      }
+    } else if (endIndex == -1) {
+      i = path.length();
+    }
+    int endi = i;
+    // Add last segment if one exists for verification
+    if (i == n && i > driveStrLength) {
+      ++segmentIndex;
+    }
+    if (beginIndex > segmentIndex || endIndex > segmentIndex) {
+      throw new IndexOutOfBoundsException(
+          String.format("path: %s, beginIndex: %d endIndex: %d", toString(), beginIndex, endIndex));
+    }
+    // If beginIndex is 0 we include the drive. Very odd semantics.
+    int driveStrLength = 0;
+    if (beginIndex == 0) {
+      starti = 0;
+      driveStrLength = this.driveStrLength;
+      endi = Math.max(endi, driveStrLength);
+    }
+    return new PathFragment(path.substring(starti, endi), driveStrLength);
+  }
+
+  /**
+   * Returns the segments of this path fragment. This array should not be
+   * modified.
+   */
+  String[] segments() {
+    int segmentCount = segmentCount();
+    String[] segments = new String[segmentCount];
+    int segmentIndex = 0;
+    int nexti = driveStrLength;
+    int n = path.length();
+    for (int i = driveStrLength; i < n; ++i) {
+      if (path.charAt(i) == SEPARATOR_CHAR) {
+        segments[segmentIndex++] = path.substring(nexti, i);
+        nexti = i + 1;
+      }
+    }
+    // Add last segment if one exists.
+    if (nexti < n) {
+      segments[segmentIndex] = path.substring(nexti);
+    }
+    return segments;
+  }
+
+  /**
+   * Returns a list of the segments.
+   *
+   * <p>This operation is O(N) on the length of the string.
+   */
+  public ImmutableList<String> getSegments() {
+    return ImmutableList.copyOf(segments());
+  }
+
+  public int getFirstSegment(Set<String> values) {
+    String[] segments = segments();
+    for (int i = 0; i < segments.length; ++i) {
+      if (values.contains(segments[i])) {
+        return i;
+      }
+    }
+    return INVALID_SEGMENT;
+  }
+
+  /** Returns the path string, or '.' if the path is empty. */
   public String getSafePathString() {
-    return (!isAbsolute() && (segmentCount() == 0)) ? "." : getPathString();
+    return !path.isEmpty() ? path : ".";
   }
 
   /**
@@ -310,399 +539,52 @@
    */
   public String getCallablePathString() {
     if (isAbsolute()) {
-      return getPathString();
-    } else if (segmentCount() == 0) {
+      return path;
+    } else if (path.isEmpty()) {
       return ".";
-    } else if (segmentCount() == 1) {
-      return "." + HELPER.getPrimarySeparatorChar() + getPathString();
+    } else if (path.indexOf(SEPARATOR_CHAR) == -1) {
+      return "." + SEPARATOR_CHAR + path;
     } else {
-      return getPathString();
+      return path;
     }
   }
 
   /**
-  * Throws {@link IllegalArgumentException} if {@code paths} contains any paths that
-  * are equal to {@code startingWithPath} or that are not beneath {@code startingWithPath}.
-  */
-  public static void checkAllPathsAreUnder(Iterable<PathFragment> paths,
-      PathFragment startingWithPath) {
-    for (PathFragment path : paths) {
-      Preconditions.checkArgument(
-          !path.equals(startingWithPath) && path.startsWith(startingWithPath),
-              "%s is not beneath %s", path, startingWithPath);
-    }
-  }
-
-  private String joinSegments(char separatorChar) {
-    if (segments.length == 0 && isAbsolute()) {
-      return windowsVolume() + ROOT_DIR;
-    }
-
-    // Profile driven optimization:
-    // Preallocate a size determined by the number of segments, so that
-    // we do not have to expand the capacity of the StringBuilder.
-    // Heuristically, this estimate is right for about 99% of the time.
-    int estimateSize =
-        ((getDriveLetter() != '\0') ? 2 : 0)
-            + ((segments.length == 0) ? 0 : (segments.length + 1) * 20);
-    StringBuilder result = new StringBuilder(estimateSize);
-    if (isAbsolute()) {
-      // Only print the Windows volume label if the PathFragment is absolute. Do not print relative
-      // Windows paths like "C:foo/bar", it would break all kinds of things, e.g. glob().
-      result.append(windowsVolume());
-    }
-    boolean initialSegment = true;
-    for (String segment : segments) {
-      if (!initialSegment || isAbsolute()) {
-        result.append(separatorChar);
-      }
-      initialSegment = false;
-      result.append(segment);
-    }
-    return result.toString();
-  }
-
-  /**
-   * Return true iff none of the segments are either "." or "..".
-   */
-  public boolean isNormalized() {
-    for (String segment : segments) {
-      if (segment.equals(".") || segment.equals("..")) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  public static boolean isNormalized(String path) {
-    return PathFragment.create(path).isNormalized();
-  }
-
-  /**
-   * Normalizes the path fragment: removes "." and ".." segments if possible
-   * (if there are too many ".." segments, the resulting PathFragment will still
-   * start with "..").
-   */
-  public PathFragment normalize() {
-    String[] scratchSegments = new String[segments.length];
-    int segmentCount = 0;
-
-    for (String segment : segments) {
-      switch (segment) {
-        case ".":
-          // Just discard it
-          break;
-        case "..":
-          if (segmentCount > 0 && !scratchSegments[segmentCount - 1].equals("..")) {
-            // Remove the last segment, if there is one and it is not "..". This
-            // means that the resulting PathFragment can still contain ".."
-            // segments at the beginning.
-            segmentCount--;
-          } else {
-            scratchSegments[segmentCount++] = segment;
-          }
-          break;
-        default:
-          scratchSegments[segmentCount++] = segment;
-      }
-    }
-
-    if (segmentCount == segments.length) {
-      // Optimization, no new PathFragment needs to be created.
-      return this;
-    }
-
-    return HELPER.createAlreadyInterned(
-        getDriveLetter(), isAbsolute(), subarray(scratchSegments, 0, segmentCount));
-  }
-
-  /**
-   * Returns the path formed by appending the relative or absolute path fragment
-   * {@code otherFragment} to this path.
-   *
-   * <p>If {@code otherFragment} is absolute, the current path will be ignored;
-   * otherwise, they will be concatenated. This is a purely syntactic operation,
-   * with no path normalization or I/O performed.
-   */
-  public PathFragment getRelative(PathFragment otherFragment) {
-    if (otherFragment == EMPTY_FRAGMENT) {
-      return this;
-    }
-
-    if (otherFragment.isAbsolute()) {
-      char driveLetter = getDriveLetter();
-      return driveLetter == '\0' || otherFragment.getDriveLetter() != '\0'
-          ? otherFragment
-          : createAlreadyInterned(driveLetter, true, otherFragment.segments);
-    } else {
-      return create(this, otherFragment);
-    }
-  }
-
-  /**
-   * Returns the path formed by appending the relative or absolute string
-   * {@code path} to this path.
-   *
-   * <p>If the given path string is absolute, the current path will be ignored;
-   * otherwise, they will be concatenated. This is a purely syntactic operation,
-   * with no path normalization or I/O performed.
-   */
-  public PathFragment getRelative(String path) {
-    return getRelative(create(path));
-  }
-
-  /**
-   * Returns the path formed by appending the single non-special segment "baseName" to this path.
-   *
-   * <p>You should almost always use {@link #getRelative} instead, which has the same performance
-   * characteristics if the given name is a valid base name, and which also works for '.', '..', and
-   * strings containing '/'.
-   *
-   * @throws IllegalArgumentException if {@code baseName} is not a valid base name according to
-   *     {@link #checkBaseName}
-   */
-  public PathFragment getChild(String baseName) {
-    checkBaseName(baseName);
-    baseName = StringCanonicalizer.intern(baseName);
-    String[] newSegments = Arrays.copyOf(segments, segments.length + 1);
-    newSegments[newSegments.length - 1] = baseName;
-    return createAlreadyInterned(getDriveLetter(), isAbsolute(), newSegments);
-  }
-
-  /**
-   * Returns the last segment of this path, or "" for the empty fragment.
-   */
-  public String getBaseName() {
-    return (segments.length == 0) ? "" : segments[segments.length - 1];
-  }
-
-  /**
    * Returns the file extension of this path, excluding the period, or "" if there is no extension.
    */
   public String getFileExtension() {
-    String baseName = getBaseName();
-
-    int lastIndex = baseName.lastIndexOf('.');
-    if (lastIndex != -1) {
-      return baseName.substring(lastIndex + 1);
+    int n = path.length();
+    for (int i = n - 1; i > driveStrLength; --i) {
+      char c = path.charAt(i);
+      if (c == '.') {
+        return path.substring(i + 1, n);
+      } else if (c == SEPARATOR_CHAR) {
+        break;
+      }
     }
-
     return "";
   }
 
   /**
-   * Returns a relative path fragment to this path, relative to
-   * {@code ancestorDirectory}.
-   * <p>
-   * <code>x.relativeTo(z) == y</code> implies
-   * <code>z.getRelative(y) == x</code>.
-   * <p>
-   * For example, <code>"foo/bar/wiz".relativeTo("foo")</code>
-   * returns <code>"bar/wiz"</code>.
-   */
-  public PathFragment relativeTo(PathFragment ancestorDirectory) {
-    String[] ancestorSegments = ancestorDirectory.segments();
-    int ancestorLength = ancestorSegments.length;
-
-    if (isAbsolute() != ancestorDirectory.isAbsolute() || segments.length < ancestorLength) {
-      throw new IllegalArgumentException("PathFragment " + this
-          + " is not beneath " + ancestorDirectory);
-    }
-
-    if (!HELPER.segmentsEqual(ancestorLength, segments, 0, ancestorSegments)) {
-      throw new IllegalArgumentException(
-          "PathFragment " + this + " is not beneath " + ancestorDirectory);
-    }
-
-    int length = segments.length - ancestorLength;
-    String[] resultSegments = subarray(segments, ancestorLength, length);
-    return createAlreadyInterned('\0', false, resultSegments);
-  }
-
-  /**
-   * Returns a relative path fragment to this path, relative to {@code path}.
-   */
-  public PathFragment relativeTo(String path) {
-    return relativeTo(create(path));
-  }
-
-  /**
-   * Returns a new PathFragment formed by appending {@code newName} to the
-   * parent directory. Null is returned iff this method is called on a
-   * PathFragment with zero segments.  If {@code newName} designates an absolute path,
-   * the value of {@code this} will be ignored and a PathFragment corresponding to
-   * {@code newName} will be returned.  This behavior is consistent with the behavior of
-   * {@link #getRelative(String)}.
+   * Returns a new PathFragment formed by appending {@code newName} to the parent directory. Null is
+   * returned iff this method is called on a PathFragment with zero segments. If {@code newName}
+   * designates an absolute path, the value of {@code this} will be ignored and a PathFragment
+   * corresponding to {@code newName} will be returned. This behavior is consistent with the
+   * behavior of {@link #getRelative(String)}.
    */
   public PathFragment replaceName(String newName) {
-    return segments.length == 0 ? null : getParentDirectory().getRelative(newName);
+    PathFragment parent = getParentDirectory();
+    return parent != null ? parent.getRelative(newName) : null;
   }
 
   /**
-   * Returns a path representing the parent directory of this path,
-   * or null iff this Path represents the root of the filesystem.
+   * Returns the drive for an absolute path fragment.
    *
-   * <p>Note: This method DOES NOT normalize ".."  and "." path segments.
+   * <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/".
    */
-  public PathFragment getParentDirectory() {
-    return segments.length == 0 ? null : subFragment(0, segments.length - 1);
-  }
-
-  /**
-   * Returns true iff {@code prefix}, considered as a list of path segments, is
-   * a prefix of {@code this}, and that they are both relative or both
-   * absolute.
-   *
-   * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
-   * order)
-   */
-  public boolean startsWith(PathFragment prefix) {
-    if (isAbsolute() != prefix.isAbsolute()
-        || this.segments.length < prefix.segments.length
-        || (isAbsolute() && getDriveLetter() != prefix.getDriveLetter())) {
-      return false;
-    }
-    return HELPER.segmentsEqual(prefix.segments.length, segments, 0, prefix.segments);
-  }
-
-  /**
-   * Returns true iff {@code suffix}, considered as a list of path segments, is
-   * relative and a suffix of {@code this}, or both are absolute and equal.
-   *
-   * <p>This is a reflexive, transitive, anti-symmetric relation (i.e. a partial
-   * order)
-   */
-  public boolean endsWith(PathFragment suffix) {
-    if ((suffix.isAbsolute() && !suffix.equals(this))
-        || this.segments.length < suffix.segments.length) {
-      return false;
-    }
-    int offset = this.segments.length - suffix.segments.length;
-    return HELPER.segmentsEqual(suffix.segments.length, segments, offset, suffix.segments);
-  }
-
-  private static String[] subarray(String[] array, int start, int length) {
-    String[] subarray = new String[length];
-    System.arraycopy(array, start, subarray, 0, length);
-    return subarray;
-  }
-
-  /**
-   * Returns a new path fragment that is a sub fragment of this one.
-   * The sub fragment begins at the specified <code>beginIndex</code> segment
-   * and ends at the segment at index <code>endIndex - 1</code>. Thus the number
-   * of segments in the new PathFragment is <code>endIndex - beginIndex</code>.
-   *
-   * @param      beginIndex   the beginning index, inclusive.
-   * @param      endIndex     the ending index, exclusive.
-   * @return     the specified sub fragment, never null.
-   * @exception  IndexOutOfBoundsException  if the
-   *             <code>beginIndex</code> is negative, or
-   *             <code>endIndex</code> is larger than the length of
-   *             this <code>String</code> object, or
-   *             <code>beginIndex</code> is larger than
-   *             <code>endIndex</code>.
-   */
-  public PathFragment subFragment(int beginIndex, int endIndex) {
-    int count = segments.length;
-    if ((beginIndex < 0) || (beginIndex > endIndex) || (endIndex > count)) {
-      throw new IndexOutOfBoundsException(String.format("path: %s, beginIndex: %d endIndex: %d",
-          toString(), beginIndex, endIndex));
-    }
-    boolean isAbsolute = (beginIndex == 0) && isAbsolute();
-    return ((beginIndex == 0) && (endIndex == count))
-        ? this
-        : createAlreadyInterned(
-            getDriveLetter(), isAbsolute, subarray(segments, beginIndex, endIndex - beginIndex));
-  }
-
-  /**
-   * Returns a new path fragment that is a sub fragment of this one. The sub fragment begins at the
-   * specified <code>beginIndex</code> segment and contains the rest of the original path fragment.
-   *
-   * @param beginIndex the beginning index, inclusive.
-   * @return the specified sub fragment, never null.
-   * @exception IndexOutOfBoundsException if the <code>beginIndex</code> is negative, or <code>
-   *     endIndex</code> is larger than the length of this <code>String</code> object, or <code>
-   *     beginIndex</code> is larger than <code>endIndex</code>.
-   */
-  public PathFragment subFragment(int beginIndex) {
-    return subFragment(beginIndex, segments.length);
-  }
-
-  /**
-   * Returns true iff the path represented by this object is absolute.
-   *
-   * <p>True both for UNIX-style absolute paths ("/foo") and Windows-style ("C:/foo"). False for a
-   * Windows-style volume label ("C:") which is actually a relative path.
-   */
-  public abstract boolean isAbsolute();
-
-  public static boolean isAbsolute(String path) {
-    return PathFragment.create(path).isAbsolute();
-  }
-
-  /**
-   * Returns the segments of this path fragment. This array should not be
-   * modified.
-   */
-  String[] segments() {
-    return segments;
-  }
-
-  public ImmutableList<String> getSegments() {
-    return ImmutableList.copyOf(segments);
-  }
-
-  public abstract String windowsVolume();
-
-  /** Return the drive letter or '\0' if not applicable. */
-  // TODO(bazel-team): This doesn't need to pollute the PathFragment interface (ditto for
-  // windowsVolume).
-  public abstract char getDriveLetter();
-
-  public boolean isEmpty() {
-    return segments.length == 0;
-  }
-
-  /**
-   * Returns the number of segments in this path.
-   */
-  public int segmentCount() {
-    return segments.length;
-  }
-
-  /**
-   * Returns the specified segment of this path; index must be positive and
-   * less than numSegments().
-   */
-  public String getSegment(int index) {
-    return segments[index];
-  }
-
-  /**
-   * Returns the index of the first segment which equals one of the input values
-   * or {@link PathFragment#INVALID_SEGMENT} if none of the segments match.
-   */
-  public int getFirstSegment(Set<String> values) {
-    for (int i = 0; i < segments.length; i++) {
-      if (values.contains(segments[i])) {
-        return i;
-      }
-    }
-    return INVALID_SEGMENT;
-  }
-
-  /**
-   * Returns true iff this path contains uplevel references "..".
-   */
-  public boolean containsUplevelReferences() {
-    for (String segment : segments) {
-      if (segment.equals("..")) {
-        return true;
-      }
-    }
-    return false;
+  public String getDriveStr() {
+    Preconditions.checkArgument(isAbsolute());
+    return path.substring(0, driveStrLength);
   }
 
   /**
@@ -711,60 +593,129 @@
    */
   public PathFragment toRelative() {
     Preconditions.checkArgument(isAbsolute());
-    return HELPER.createAlreadyInterned(getDriveLetter(), false, segments);
+    return new PathFragment(path.substring(driveStrLength), 0);
   }
 
-  @Override
-  public final int hashCode() {
-    // We use the hash code caching strategy employed by java.lang.String. There are three subtle
-    // things going on here:
-    //
-    // (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet.
-    // Yes, this means that if the hash code is really 0 then we will "recompute" it each time. But
-    // this isn't a problem in practice since a hash code of 0 is rare.
-    //
-    // (2) Since we have no synchronization, multiple threads can race here thinking they are the
-    // first one to compute and cache the hash code.
-    //
-    // (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one
-    // thread may not be visible by another. Note that we don't need to worry about multiple
-    // inefficient reads of 'hashCode' on the same thread since it's non-volatile.
-    //
-    // All three of these issues are benign from a correctness perspective; in the end we have no
-    // overhead from synchronization, at the cost of potentially computing the hash code more than
-    // once.
-    if (hashCode == 0) {
-      hashCode = computeHashCode();
-    }
-    return hashCode;
-  }
-
-  protected abstract int computeHashCode();
-
-  @Override
-  public abstract boolean equals(Object other);
-
   /**
-   * Compares two PathFragments using the lexicographical order.
+   * Returns true if this path contains uplevel references "..".
+   *
+   * <p>Since path fragments are normalized, this implies that the uplevel reference is at the start
+   * of the path fragment.
    */
-  @Override
-  public int compareTo(PathFragment p2) {
-    return HELPER.compare(this, p2);
+  public boolean containsUplevelReferences() {
+    // Path is normalized, so any ".." would have to be at the very start
+    return path.startsWith("..");
   }
 
-  @Override
-  public String toString() {
-    return getPathString();
+  /**
+   * Returns true if the passed path contains uplevel references ".." or single-dot references "."
+   *
+   * <p>This is useful to check a string for normalization before constructing a PathFragment, since
+   * these are always normalized and will throw uplevel references away.
+   */
+  public static boolean isNormalized(String path) {
+    return isNormalizedImpl(path, true /* lookForSameLevelReferences */);
   }
 
-  @Override
-  public void repr(SkylarkPrinter printer) {
-    printer.append(getPathString());
+  /**
+   * Returns true if the passed path contains uplevel references "..".
+   *
+   * <p>This is useful to check a string for '..' segments before constructing a PathFragment, since
+   * these are always normalized and will throw uplevel references away.
+   */
+  public static boolean containsUplevelReferences(String path) {
+    return !isNormalizedImpl(path, false /* lookForSameLevelReferences */);
+  }
+
+  private enum NormalizedImplState {
+    Base, /* No particular state, eg. an 'a' or 'L' character */
+    Separator, /* We just saw a separator */
+    Dot, /* We just saw a dot after a separator */
+    DotDot, /* We just saw two dots after a separator */
+  }
+
+  private static boolean isNormalizedImpl(String path, boolean lookForSameLevelReferences) {
+    // Starting state is equivalent to having just seen a separator
+    NormalizedImplState state = NormalizedImplState.Separator;
+    int n = path.length();
+    for (int i = 0; i < n; ++i) {
+      char c = path.charAt(i);
+      boolean isSeparator = OS.isSeparator(c);
+      switch (state) {
+        case Base:
+          if (isSeparator) {
+            state = NormalizedImplState.Separator;
+          } else {
+            state = NormalizedImplState.Base;
+          }
+          break;
+        case Separator:
+          if (isSeparator) {
+            state = NormalizedImplState.Separator;
+          } else if (c == '.') {
+            state = NormalizedImplState.Dot;
+          } else {
+            state = NormalizedImplState.Base;
+          }
+          break;
+        case Dot:
+          if (isSeparator) {
+            if (lookForSameLevelReferences) {
+              // "." segment found
+              return false;
+            }
+            state = NormalizedImplState.Separator;
+          } else if (c == '.') {
+            state = NormalizedImplState.DotDot;
+          } else {
+            state = NormalizedImplState.Base;
+          }
+          break;
+        case DotDot:
+          if (isSeparator) {
+            // ".." segment found
+            return false;
+          } else {
+            state = NormalizedImplState.Base;
+          }
+          break;
+        default:
+          throw new IllegalStateException("Unhandled state: " + state);
+      }
+    }
+    // The character just after the string is equivalent to a separator
+    switch (state) {
+      case Dot:
+        if (lookForSameLevelReferences) {
+          // "." segment found
+          return false;
+        }
+        break;
+      case DotDot:
+        return false;
+      default:
+    }
+    return true;
+  }
+
+  /**
+   * Throws {@link IllegalArgumentException} if {@code paths} contains any paths that are equal to
+   * {@code startingWithPath} or that are not beneath {@code startingWithPath}.
+   */
+  public static void checkAllPathsAreUnder(
+      Iterable<PathFragment> paths, PathFragment startingWithPath) {
+    for (PathFragment path : paths) {
+      Preconditions.checkArgument(
+          !path.equals(startingWithPath) && path.startsWith(startingWithPath),
+          "%s is not beneath %s",
+          path,
+          startingWithPath);
+    }
   }
 
   @Override
   public String filePathForFileTypeMatcher() {
-    return getBaseName();
+    return path;
   }
 
   @Override
@@ -783,25 +734,13 @@
     @Override
     public void serialize(PathFragment pathFragment, CodedOutputStream codedOut)
         throws IOException, SerializationException {
-      codedOut.writeInt32NoTag(pathFragment.getDriveLetter());
-      codedOut.writeBoolNoTag(pathFragment.isAbsolute());
-      codedOut.writeInt32NoTag(pathFragment.segmentCount());
-      for (int i = 0; i < pathFragment.segmentCount(); i++) {
-        stringCodec.serialize(pathFragment.getSegment(i), codedOut);
-      }
+      stringCodec.serialize(pathFragment.getPathString(), codedOut);
     }
 
     @Override
     public PathFragment deserialize(CodedInputStream codedIn)
         throws IOException, SerializationException {
-      char driveLetter = (char) codedIn.readInt32();
-      boolean isAbsolute = codedIn.readBool();
-      int segmentCount = codedIn.readInt32();
-      String[] segments = new String[segmentCount];
-      for (int i = 0; i < segmentCount; i++) {
-        segments[i] = stringCodec.deserialize(codedIn);
-      }
-      return PathFragment.create(driveLetter, isAbsolute, segments);
+      return PathFragment.createAlreadyNormalized(stringCodec.deserialize(codedIn));
     }
   }
 
@@ -816,4 +755,8 @@
       throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'");
     }
   }
+
+  private Object writeReplace() {
+    return new PathFragmentSerializationProxy(path);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
index 39aefd1..4ba66be 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragmentSerializationProxy.java
@@ -17,7 +17,6 @@
 import java.io.IOException;
 import java.io.ObjectOutput;
 
-
 /**
  * A helper proxy for serializing immutable {@link PathFragment} objects.
  */
@@ -44,7 +43,7 @@
   }
 
   private Object readResolve() {
-    return PathFragment.create(pathFragmentString);
+    return PathFragment.createAlreadyNormalized(pathFragmentString);
   }
 }
 
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/RootedPath.java b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
index 9182f10..09ab313 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/RootedPath.java
@@ -30,9 +30,8 @@
  * <p>Two {@link RootedPath}s are considered equal iff they have equal roots and equal relative
  * paths.
  *
- * <p>TODO(bazel-team): refactor Artifact to use this instead of Root. TODO(bazel-team): use an
- * opaque root representation so as to not expose the absolute path to clients via #asPath or
- * #getRoot.
+ * <p>TODO(bazel-team): use an opaque root representation so as to not expose the absolute path to
+ * clients via #asPath or #getRoot.
  */
 public class RootedPath implements Serializable {
 
@@ -48,7 +47,7 @@
         rootRelativePath,
         root);
     this.root = root;
-    this.rootRelativePath = rootRelativePath.normalize();
+    this.rootRelativePath = rootRelativePath;
     this.path = root.getRelative(this.rootRelativePath);
   }
 
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 d924e19..a929f14 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 PathFragment prefix;
+    final FileSystem fileSystem;
+
+    public FileSystemAndPrefix(PathFragment 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.
@@ -65,11 +76,12 @@
     Preconditions.checkNotNull(rootFileSystem);
     Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
     Preconditions.checkArgument(
-        !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
+        prefixMapping.keySet().stream().noneMatch(p -> p.getPathString().equals("/")),
         "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()) {
@@ -80,9 +92,13 @@
       PathFragment 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(Path 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());
+  Path adjustPath(Path path, FileSystem delegate) {
+    return delegate.getPath(path.getPathString());
   }
 
   /**
@@ -344,12 +365,7 @@
     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;
+    return delegate.getDirectoryEntries(resolvedPath);
   }
 
   // No need for the more complex logic of getDirectoryEntries; it calls it implicitly.
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java
new file mode 100644
index 0000000..8576643
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java
@@ -0,0 +1,138 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.vfs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+@VisibleForTesting
+class UnixOsPathPolicy implements OsPathPolicy {
+
+  static final UnixOsPathPolicy INSTANCE = new UnixOsPathPolicy();
+  private static final Splitter PATH_SPLITTER = Splitter.onPattern("/+").omitEmptyStrings();
+
+  @Override
+  public int needsToNormalize(String path) {
+    int n = path.length();
+    int dotCount = 0;
+    char prevChar = 0;
+    for (int i = 0; i < n; i++) {
+      char c = path.charAt(i);
+      if (c == '\\') {
+        return NEEDS_NORMALIZE;
+      }
+      if (c == '/') {
+        if (prevChar == '/') {
+          return NEEDS_NORMALIZE;
+        }
+        if (dotCount == 1 || dotCount == 2) {
+          return NEEDS_NORMALIZE;
+        }
+      }
+      dotCount = c == '.' ? dotCount + 1 : 0;
+      prevChar = c;
+    }
+    if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
+      return NEEDS_NORMALIZE;
+    }
+    return NORMALIZED;
+  }
+
+  @Override
+  public int needsToNormalizeSuffix(String normalizedSuffix) {
+    // We know that the string is normalized
+    // In this case only suffixes starting with ".." may cause
+    // normalization once concatenated with other strings
+    return normalizedSuffix.startsWith("..") ? NEEDS_NORMALIZE : NORMALIZED;
+  }
+
+  @Override
+  public String normalize(String path, int normalizationLevel) {
+    if (normalizationLevel == NORMALIZED) {
+      return path;
+    }
+    if (path.isEmpty()) {
+      return path;
+    }
+    boolean isAbsolute = path.charAt(0) == '/';
+    StringBuilder sb = new StringBuilder(path.length());
+    if (isAbsolute) {
+      sb.append('/');
+    }
+    String[] segments = Iterables.toArray(PATH_SPLITTER.splitToList(path), String.class);
+    int segmentCount = Utils.removeRelativePaths(segments, 0, isAbsolute);
+    for (int i = 0; i < segmentCount; ++i) {
+      sb.append(segments[i]);
+      sb.append('/');
+    }
+    if (segmentCount > 0) {
+      sb.deleteCharAt(sb.length() - 1);
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public int getDriveStrLength(String path) {
+    if (path.length() == 0) {
+      return 0;
+    }
+    return (path.charAt(0) == '/') ? 1 : 0;
+  }
+
+  @Override
+  public int compare(String s1, String s2) {
+    return s1.compareTo(s2);
+  }
+
+  @Override
+  public int compare(char c1, char c2) {
+    return Character.compare(c1, c2);
+  }
+
+  @Override
+  public boolean equals(String s1, String s2) {
+    return s1.equals(s2);
+  }
+
+  @Override
+  public int hash(String s) {
+    return s.hashCode();
+  }
+
+  @Override
+  public boolean startsWith(String path, String prefix) {
+    return path.startsWith(prefix);
+  }
+
+  @Override
+  public boolean endsWith(String path, String suffix) {
+    return path.endsWith(suffix);
+  }
+
+  @Override
+  public char getSeparator() {
+    return '/';
+  }
+
+  @Override
+  public boolean isSeparator(char c) {
+    return c == '/';
+  }
+
+  @Override
+  public boolean isCaseSensitive() {
+    return true;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java
deleted file mode 100644
index b581d43..0000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/UnixPathFragment.java
+++ /dev/null
@@ -1,220 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import com.google.common.base.Preconditions;
-import java.io.InvalidObjectException;
-import java.io.ObjectInputStream;
-
-/**
- * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run
- * on a non-Windows platform.
- */
-abstract class UnixPathFragment extends PathFragment {
-  static final Helper HELPER = new Helper();
-  /**
-    * We have two concrete subclasses with zero per-instance additional memory overhead. Do not add
-    * any fields. See the comment on memory use in PathFragment for details on the current
-    * per-instance memory usage.
-    */
-
-  protected UnixPathFragment(String[] segments) {
-    super(segments);
-  }
-
-  @Override
-  protected int computeHashCode() {
-    int h = 0;
-    for (String segment : segments) {
-      int segmentHash = segment.hashCode();
-      h = h * 31 + segmentHash;
-    }
-    return h;
-  }
-
-  @Override
-  public String windowsVolume() {
-    return "";
-  }
-
-  @Override
-  public char getDriveLetter() {
-    return '\0';
-  }
-
-  private static class Helper extends PathFragment.Helper {
-    private static final char SEPARATOR_CHAR = '/';
-
-    @Override
-    PathFragment create(String path) {
-      boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
-      return isAbsolute
-          ? new AbsoluteUnixPathFragment(segment(path, 1))
-          : new RelativeUnixPathFragment(segment(path, 0));
-    }
-
-    @Override
-    PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) {
-      Preconditions.checkState(driveLetter == '\0', driveLetter);
-      return isAbsolute
-          ? new AbsoluteUnixPathFragment(segments)
-          : new RelativeUnixPathFragment(segments);
-    }
-
-    @Override
-    char getPrimarySeparatorChar() {
-      return SEPARATOR_CHAR;
-    }
-
-    @Override
-    boolean isSeparator(char c) {
-      return c == SEPARATOR_CHAR;
-    }
-
-    @Override
-    boolean containsSeparatorChar(String path) {
-      return path.indexOf(SEPARATOR_CHAR) != -1;
-    }
-
-    @Override
-    boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) {
-      if ((segments1.length - offset1) < length || segments2.length < length) {
-        return false;
-      }
-      for (int i = 0; i < length; ++i) {
-        String seg1 = segments1[i + offset1];
-        String seg2 = segments2[i];
-        if ((seg1 == null) != (seg2 == null)) {
-          return false;
-        }
-        if (seg1 == null) {
-          continue;
-        }
-        if (!seg1.equals(seg2)) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    @Override
-    protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) {
-      if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) {
-        return pathFragment1.isAbsolute() ? -1 : 1;
-      }
-      String[] segments1 = pathFragment1.segments();
-      String[] segments2 = pathFragment2.segments();
-      int len1 = segments1.length;
-      int len2 = segments2.length;
-      int n = Math.min(len1, len2);
-      for (int i = 0; i < n; i++) {
-        String seg1 = segments1[i];
-        String seg2 = segments2[i];
-        int cmp = seg1.compareTo(seg2);
-        if (cmp != 0) {
-          return cmp;
-        }
-      }
-      return len1 - len2;
-    }
-  }
-
-  private static final class AbsoluteUnixPathFragment extends UnixPathFragment {
-    private AbsoluteUnixPathFragment(String[] segments) {
-      super(segments);
-    }
-
-    @Override
-    public boolean isAbsolute() {
-      return true;
-    }
-
-    @Override
-    protected int computeHashCode() {
-      int h = Boolean.TRUE.hashCode();
-      h = h * 31 + super.computeHashCode();
-      return h;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (!(other instanceof AbsoluteUnixPathFragment)) {
-        return false;
-      }
-      if (this == other) {
-        return true;
-      }
-      AbsoluteUnixPathFragment otherAbsoluteUnixPathFragment = (AbsoluteUnixPathFragment) other;
-      return HELPER.segmentsEqual(this.segments, otherAbsoluteUnixPathFragment.segments);
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected Object writeReplace() {
-      return super.writeReplace();
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
-      super.readObject(stream);
-    }
-  }
-
-  private static final class RelativeUnixPathFragment extends UnixPathFragment {
-    private RelativeUnixPathFragment(String[] segments) {
-      super(segments);
-    }
-
-    @Override
-    public boolean isAbsolute() {
-      return false;
-    }
-
-    @Override
-    protected int computeHashCode() {
-      int h = Boolean.FALSE.hashCode();
-      h = h * 31 + super.computeHashCode();
-      return h;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (!(other instanceof RelativeUnixPathFragment)) {
-        return false;
-      }
-      if (this == other) {
-        return true;
-      }
-      RelativeUnixPathFragment otherRelativeUnixPathFragment = (RelativeUnixPathFragment) other;
-      return HELPER.segmentsEqual(this.segments, otherRelativeUnixPathFragment.segments);
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected Object writeReplace() {
-      return super.writeReplace();
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
-      super.readObject(stream);
-    }
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java
new file mode 100644
index 0000000..f420683
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java
@@ -0,0 +1,240 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.vfs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.windows.WindowsShortPath;
+import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
+import java.io.IOException;
+
+@VisibleForTesting
+class WindowsOsPathPolicy implements OsPathPolicy {
+
+  static final WindowsOsPathPolicy INSTANCE = new WindowsOsPathPolicy();
+
+  static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1;
+
+  private static final Splitter WINDOWS_PATH_SPLITTER =
+      Splitter.onPattern("[\\\\/]+").omitEmptyStrings();
+
+  private final ShortPathResolver shortPathResolver;
+
+  interface ShortPathResolver {
+    String resolveShortPath(String path);
+  }
+
+  static class DefaultShortPathResolver implements ShortPathResolver {
+    @Override
+    public String resolveShortPath(String path) {
+      try {
+        return WindowsFileOperations.getLongPath(path);
+      } catch (IOException e) {
+        return path;
+      }
+    }
+  }
+
+  WindowsOsPathPolicy() {
+    this(new DefaultShortPathResolver());
+  }
+
+  WindowsOsPathPolicy(ShortPathResolver shortPathResolver) {
+    this.shortPathResolver = shortPathResolver;
+  }
+
+  @Override
+  public int needsToNormalize(String path) {
+    int n = path.length();
+    int normalizationLevel = NORMALIZED;
+    int dotCount = 0;
+    char prevChar = 0;
+    int segmentBeginIndex = 0; // The start index of the current path index
+    boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test
+    for (int i = 0; i < n; i++) {
+      char c = path.charAt(i);
+      if (isSeparator(c)) {
+        if (c == '\\') {
+          normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+        }
+        // No need to check for '\\' here because that already causes normalization
+        if (prevChar == '/') {
+          normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+        }
+        if (dotCount == 1 || dotCount == 2) {
+          normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+        }
+        if (segmentHasShortPathChar) {
+          if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) {
+            normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
+          }
+        }
+        segmentBeginIndex = i + 1;
+        segmentHasShortPathChar = false;
+      } else if (c == '~') {
+        // This path segment might be a Windows short path segment
+        segmentHasShortPathChar = true;
+      }
+      dotCount = c == '.' ? dotCount + 1 : 0;
+      prevChar = c;
+    }
+    if (segmentHasShortPathChar) {
+      if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex))) {
+        normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
+      }
+    }
+    if ((n > 1 && isSeparator(prevChar)) || dotCount == 1 || dotCount == 2) {
+      normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
+    }
+    return normalizationLevel;
+  }
+
+  @Override
+  public int needsToNormalizeSuffix(String normalizedSuffix) {
+    // On Windows, all bets are off because of short paths, so we have to check the entire string
+    return needsToNormalize(normalizedSuffix);
+  }
+
+  @Override
+  public String normalize(String path, int normalizationLevel) {
+    if (normalizationLevel == NORMALIZED) {
+      return path;
+    }
+    if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) {
+      String resolvedPath = shortPathResolver.resolveShortPath(path);
+      if (resolvedPath != null) {
+        path = resolvedPath;
+      }
+    }
+    String[] segments = Iterables.toArray(WINDOWS_PATH_SPLITTER.splitToList(path), String.class);
+    int driveStrLength = getDriveStrLength(path);
+    boolean isAbsolute = driveStrLength > 0;
+    int segmentSkipCount = isAbsolute && driveStrLength > 1 ? 1 : 0;
+
+    StringBuilder sb = new StringBuilder(path.length());
+    if (isAbsolute) {
+      char c = path.charAt(0);
+      if (isSeparator(c)) {
+        sb.append('/');
+      } else {
+        sb.append(Character.toUpperCase(c));
+        sb.append(":/");
+      }
+    }
+    int segmentCount = Utils.removeRelativePaths(segments, segmentSkipCount, isAbsolute);
+    for (int i = 0; i < segmentCount; ++i) {
+      sb.append(segments[i]);
+      sb.append('/');
+    }
+    if (segmentCount > 0) {
+      sb.deleteCharAt(sb.length() - 1);
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public int getDriveStrLength(String path) {
+    int n = path.length();
+    if (n == 0) {
+      return 0;
+    }
+    char c0 = path.charAt(0);
+    if (isSeparator(c0)) {
+      return 1;
+    }
+    if (n < 3) {
+      return 0;
+    }
+    char c1 = path.charAt(1);
+    char c2 = path.charAt(2);
+    if (isDriveLetter(c0) && c1 == ':' && isSeparator(c2)) {
+      return 3;
+    }
+    return 0;
+  }
+
+  private static boolean isDriveLetter(char c) {
+    return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
+  }
+
+  @Override
+  public int compare(String s1, String s2) {
+    // Windows is case-insensitive
+    return s1.compareToIgnoreCase(s2);
+  }
+
+  @Override
+  public int compare(char c1, char c2) {
+    return Character.compare(Character.toLowerCase(c1), Character.toLowerCase(c2));
+  }
+
+  @Override
+  public boolean equals(String s1, String s2) {
+    return s1.equalsIgnoreCase(s2);
+  }
+
+  @Override
+  public int hash(String s) {
+    // Windows is case-insensitive
+    return s.toLowerCase().hashCode();
+  }
+
+  @Override
+  public boolean startsWith(String path, String prefix) {
+    int pathn = path.length();
+    int prefixn = prefix.length();
+    if (pathn < prefixn) {
+      return false;
+    }
+    for (int i = 0; i < prefixn; ++i) {
+      if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean endsWith(String path, String suffix) {
+    int pathn = path.length();
+    int suffixLength = suffix.length();
+    if (pathn < suffixLength) {
+      return false;
+    }
+    int offset = pathn - suffixLength;
+    for (int i = 0; i < suffixLength; ++i) {
+      if (Character.toLowerCase(path.charAt(i + offset))
+          != Character.toLowerCase(suffix.charAt(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public char getSeparator() {
+    return '/';
+  }
+
+  @Override
+  public boolean isSeparator(char c) {
+    return c == '/' || c == '\\';
+  }
+
+  @Override
+  public boolean isCaseSensitive() {
+    return false;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java
deleted file mode 100644
index ac3be0f..0000000
--- a/src/main/java/com/google/devtools/build/lib/vfs/WindowsPathFragment.java
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import java.io.InvalidObjectException;
-import java.io.ObjectInputStream;
-
-/**
- * Abstract base class for {@link PathFragment} instances that will be allocated when Blaze is run
- * on a Windows platform.
- */
-abstract class WindowsPathFragment extends PathFragment {
-  static final Helper HELPER = new Helper();
-
-  // The drive letter of an absolute path, eg. 'C' for 'C:/foo'.
-  // We deliberately ignore "C:foo" style paths and treat them like a literal "C:foo" path segment.
-  protected final char driveLetter;
-
-  protected WindowsPathFragment(char driveLetter, String[] segments) {
-    super(segments);
-    this.driveLetter = driveLetter;
-  }
-
-  @Override
-  public String windowsVolume() {
-    return (driveLetter != '\0') ? driveLetter + ":" : "";
-  }
-
-  @Override
-  public char getDriveLetter() {
-    return driveLetter;
-  }
-
-  @Override
-  protected int computeHashCode() {
-    int h = 0;
-    for (String segment : segments) {
-      int segmentHash = segment.toLowerCase().hashCode();
-      h = h * 31 + segmentHash;
-    }
-    return h;
-  }
-
-  private static class Helper extends PathFragment.Helper {
-    private static final char SEPARATOR_CHAR = '/';
-    // TODO(laszlocsomor): Lots of internal PathFragment operations, e.g. getPathString, use the
-    // primary separator char and do not use this.
-    private static final char EXTRA_SEPARATOR_CHAR = '\\';
-
-    private static boolean isDriveLetter(char c) {
-      return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
-    }
-
-    @Override
-    PathFragment create(String path) {
-      char driveLetter =
-          path.length() >= 3
-                  && path.charAt(1) == ':'
-                  && isSeparator(path.charAt(2))
-                  && isDriveLetter(path.charAt(0))
-              ? Character.toUpperCase(path.charAt(0))
-              : '\0';
-      if (driveLetter != '\0') {
-        path = path.substring(2);
-      }
-      boolean isAbsolute = path.length() > 0 && isSeparator(path.charAt(0));
-      return isAbsolute
-          ? new AbsoluteWindowsPathFragment(driveLetter, segment(path, 1))
-          : new RelativeWindowsPathFragment(driveLetter, segment(path, 0));
-    }
-
-    @Override
-    PathFragment createAlreadyInterned(char driveLetter, boolean isAbsolute, String[] segments) {
-      return isAbsolute
-          ? new AbsoluteWindowsPathFragment(driveLetter, segments)
-          : new RelativeWindowsPathFragment(driveLetter, segments);
-    }
-
-    @Override
-    char getPrimarySeparatorChar() {
-      return SEPARATOR_CHAR;
-    }
-
-    @Override
-    boolean isSeparator(char c) {
-      return c == SEPARATOR_CHAR || c == EXTRA_SEPARATOR_CHAR;
-    }
-
-    @Override
-    boolean containsSeparatorChar(String path) {
-      // TODO(laszlocsomor): This is inefficient.
-      return path.indexOf(SEPARATOR_CHAR) != -1 || path.indexOf(EXTRA_SEPARATOR_CHAR) != -1;
-    }
-
-    @Override
-    boolean segmentsEqual(int length, String[] segments1, int offset1, String[] segments2) {
-      if ((segments1.length - offset1) < length || segments2.length < length) {
-        return false;
-      }
-      for (int i = 0; i < length; ++i) {
-        String seg1 = segments1[i + offset1];
-        String seg2 = segments2[i];
-        if ((seg1 == null) != (seg2 == null)) {
-          return false;
-        }
-        if (seg1 == null) {
-          continue;
-        }
-        // TODO(laszlocsomor): The calls to String#toLowerCase are inefficient and potentially
-        // repeated too. Also, why not use String#equalsIgnoreCase.
-        seg1 = seg1.toLowerCase();
-        seg2 = seg2.toLowerCase();
-        if (!seg1.equals(seg2)) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    @Override
-    protected int compare(PathFragment pathFragment1, PathFragment pathFragment2) {
-      if (pathFragment1.isAbsolute() != pathFragment2.isAbsolute()) {
-        return pathFragment1.isAbsolute() ? -1 : 1;
-      }
-      int cmp = Character.compare(pathFragment1.getDriveLetter(), pathFragment2.getDriveLetter());
-      if (cmp != 0) {
-        return cmp;
-      }
-      String[] segments1 = pathFragment1.segments();
-      String[] segments2 = pathFragment2.segments();
-      int len1 = segments1.length;
-      int len2 = segments2.length;
-      int n = Math.min(len1, len2);
-      for (int i = 0; i < n; i++) {
-        String seg1 = segments1[i].toLowerCase();
-        String seg2 = segments2[i].toLowerCase();
-        cmp = seg1.compareTo(seg2);
-        if (cmp != 0) {
-          return cmp;
-        }
-      }
-      return len1 - len2;
-    }
-  }
-
-  private static final class AbsoluteWindowsPathFragment extends WindowsPathFragment {
-    private AbsoluteWindowsPathFragment(char driveLetter, String[] segments) {
-      super(driveLetter, segments);
-    }
-
-    @Override
-    public boolean isAbsolute() {
-      return true;
-    }
-
-    @Override
-    protected int computeHashCode() {
-      int h = Boolean.TRUE.hashCode();
-      h = h * 31 + super.computeHashCode();
-      h = h * 31 + Character.valueOf(getDriveLetter()).hashCode();
-      return h;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (!(other instanceof AbsoluteWindowsPathFragment)) {
-        return false;
-      }
-      if (this == other) {
-        return true;
-      }
-      AbsoluteWindowsPathFragment otherAbsoluteWindowsPathFragment =
-          (AbsoluteWindowsPathFragment) other;
-      return this.driveLetter == otherAbsoluteWindowsPathFragment.driveLetter
-          && HELPER.segmentsEqual(this.segments, otherAbsoluteWindowsPathFragment.segments);
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected Object writeReplace() {
-      return super.writeReplace();
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
-      super.readObject(stream);
-    }
-  }
-
-  private static final class RelativeWindowsPathFragment extends WindowsPathFragment {
-    private RelativeWindowsPathFragment(char driveLetter, String[] segments) {
-      super(driveLetter, segments);
-    }
-
-    @Override
-    public boolean isAbsolute() {
-      return false;
-    }
-
-    @Override
-    protected int computeHashCode() {
-      int h = Boolean.FALSE.hashCode();
-      h = h * 31 + super.computeHashCode();
-      if (!isEmpty()) {
-        h = h * 31 + Character.valueOf(getDriveLetter()).hashCode();
-      }
-      return h;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (!(other instanceof RelativeWindowsPathFragment)) {
-        return false;
-      }
-      if (this == other) {
-        return true;
-      }
-      RelativeWindowsPathFragment otherRelativeWindowsPathFragment =
-          (RelativeWindowsPathFragment) other;
-      return isEmpty() && otherRelativeWindowsPathFragment.isEmpty()
-          ? true
-          : this.driveLetter == otherRelativeWindowsPathFragment.driveLetter
-              && HELPER.segmentsEqual(this.segments, otherRelativeWindowsPathFragment.segments);
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected Object writeReplace() {
-      return super.writeReplace();
-    }
-
-    // Java serialization looks for the presence of this method in the concrete class. It is not
-    // inherited from the parent class.
-    @Override
-    protected void readObject(ObjectInputStream stream) throws InvalidObjectException {
-      super.readObject(stream);
-    }
-  }
-}
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 8fd2118..c2a52a5 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
@@ -350,7 +350,8 @@
 
     Stack<String> stack = new Stack<>();
     for (Path p = path; !isRootDirectory(p); p = p.getParentDirectory()) {
-      stack.push(p.getBaseName());
+      String name = baseNameOrWindowsDrive(p);
+      stack.push(name);
     }
 
     InMemoryContentInfo inode = rootInode;
@@ -381,6 +382,13 @@
         for (int ii = segments.size() - 1; ii >= 0; --ii) {
           stack.push(segments.get(ii)); // Note this may include ".." segments.
         }
+        // Push Windows drive if there is one
+        if (linkTarget.isAbsolute()) {
+          String driveStr = linkTarget.getDriveStr();
+          if (driveStr.length() > 1) {
+            stack.push(driveStr);
+          }
+        }
       } else {
         inode = child;
       }
@@ -412,7 +420,7 @@
   private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path)
       throws IOException  {
     InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory());
-    return directoryLookup(dirInfo, path.getBaseName(), /*create=*/ false, path);
+    return directoryLookup(dirInfo, baseNameOrWindowsDrive(path), /*create=*/ false, path);
   }
 
   /**
@@ -606,7 +614,7 @@
     InMemoryDirectoryInfo parent;
     synchronized (this) {
       parent = getDirectory(path.getParentDirectory());
-      InMemoryContentInfo child = parent.getChild(path.getBaseName());
+      InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path));
       if (child != null) { // already exists
         if (child.isDirectory()) {
           return false;
@@ -618,7 +626,7 @@
       InMemoryDirectoryInfo newDir = new InMemoryDirectoryInfo(clock);
       newDir.addChild(".", newDir);
       newDir.addChild("..", parent);
-      insert(parent, path.getBaseName(), newDir, path);
+      insert(parent, baseNameOrWindowsDrive(path), newDir, path);
 
       return true;
     }
@@ -649,10 +657,11 @@
 
     synchronized (this) {
       InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory());
-      if (parent.getChild(path.getBaseName()) != null) {
+      if (parent.getChild(baseNameOrWindowsDrive(path)) != null) {
         throw Error.EEXIST.exception(path);
       }
-      insert(parent, path.getBaseName(), new InMemoryLinkInfo(clock, targetFragment), path);
+      insert(
+          parent, baseNameOrWindowsDrive(path), new InMemoryLinkInfo(clock, targetFragment), path);
     }
   }
 
@@ -703,11 +712,11 @@
 
     synchronized (this) {
       InMemoryDirectoryInfo parent = getDirectory(path.getParentDirectory());
-      InMemoryContentInfo child = parent.getChild(path.getBaseName());
+      InMemoryContentInfo child = parent.getChild(baseNameOrWindowsDrive(path));
       if (child.isDirectory() && child.getSize() > 2) {
         throw Error.ENOTEMPTY.exception(path);
       }
-      unlink(parent, path.getBaseName(), path);
+      unlink(parent, baseNameOrWindowsDrive(path), path);
       return true;
     }
   }
@@ -780,13 +789,13 @@
       InMemoryDirectoryInfo sourceParent = getDirectory(sourcePath.getParentDirectory());
       InMemoryDirectoryInfo targetParent = getDirectory(targetPath.getParentDirectory());
 
-      InMemoryContentInfo sourceInode = sourceParent.getChild(sourcePath.getBaseName());
+      InMemoryContentInfo sourceInode = sourceParent.getChild(baseNameOrWindowsDrive(sourcePath));
       if (sourceInode == null) {
         throw Error.ENOENT.exception(sourcePath);
       }
-      InMemoryContentInfo targetInode = targetParent.getChild(targetPath.getBaseName());
+      InMemoryContentInfo targetInode = targetParent.getChild(baseNameOrWindowsDrive(targetPath));
 
-      unlink(sourceParent, sourcePath.getBaseName(), sourcePath);
+      unlink(sourceParent, baseNameOrWindowsDrive(sourcePath), sourcePath);
       try {
         // TODO(bazel-team): (2009) test with symbolic links.
 
@@ -802,15 +811,19 @@
           } else if (sourceInode.isDirectory()) {
             throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.ENOTDIR + ")");
           }
-          unlink(targetParent, targetPath.getBaseName(), targetPath);
+          unlink(targetParent, baseNameOrWindowsDrive(targetPath), targetPath);
         }
         sourceInode.movedTo(targetPath);
-        insert(targetParent, targetPath.getBaseName(), sourceInode, targetPath);
+        insert(targetParent, baseNameOrWindowsDrive(targetPath), sourceInode, targetPath);
         return;
 
       } catch (IOException e) {
         sourceInode.movedTo(sourcePath);
-        insert(sourceParent, sourcePath.getBaseName(), sourceInode, sourcePath); // restore source
+        insert(
+            sourceParent,
+            baseNameOrWindowsDrive(sourcePath),
+            sourceInode,
+            sourcePath); // restore source
         throw e;
       }
     }
@@ -828,18 +841,33 @@
     synchronized (this) {
       InMemoryDirectoryInfo linkParent = getDirectory(linkPath.getParentDirectory());
       // Same check used when creating a symbolic link
-      if (linkParent.getChild(linkPath.getBaseName()) != null) {
+      if (linkParent.getChild(baseNameOrWindowsDrive(linkPath)) != null) {
         throw Error.EEXIST.exception(linkPath);
       }
       insert(
           linkParent,
-          linkPath.getBaseName(),
-          getDirectory(originalPath.getParentDirectory()).getChild(originalPath.getBaseName()),
+          baseNameOrWindowsDrive(linkPath),
+          getDirectory(originalPath.getParentDirectory())
+              .getChild(baseNameOrWindowsDrive(originalPath)),
           linkPath);
     }
   }
 
-  private boolean isRootDirectory(Path path) {
-    return path.isRootDirectory();
+  /**
+   * On Unix the root directory is "/". On Windows there isn't one, so we reach null from
+   * getParentDirectory.
+   */
+  private boolean isRootDirectory(@Nullable Path path) {
+    return path == null || path.getPathString().equals("/");
+  }
+
+  /**
+   * Returns either the base name of the path, or the drive (if referring to a Windows drive).
+   *
+   * <p>This allows the file system to treat windows drives much like directories.
+   */
+  private static String baseNameOrWindowsDrive(Path path) {
+    String name = path.getBaseName();
+    return !name.isEmpty() ? name : path.getDriveStr();
   }
 }
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..dabbbce 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
@@ -31,7 +31,7 @@
   InMemoryLinkInfo(Clock clock, PathFragment linkContent) {
     super(clock);
     this.linkContent = linkContent;
-    this.normalizedLinkContent = linkContent.normalize();
+    this.normalizedLinkContent = linkContent;
   }
 
   @Override
@@ -56,7 +56,7 @@
 
   @Override
   public long getSize() {
-    return linkContent.toString().length();
+    return linkContent.getSafePathString().length();
   }
 
   /**
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..961465e 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
@@ -14,15 +14,10 @@
 package com.google.devtools.build.lib.windows;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Function;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 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.Path;
-import com.google.devtools.build.lib.vfs.Path.PathFactory;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
 import java.io.File;
@@ -31,246 +26,11 @@
 import java.nio.file.Files;
 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 -> {
-        try {
-          // Since Path objects are created hierarchically, we know for sure that every segment of
-          // the path, except the last one, is already canonicalized, so we can return just that.
-          // Plus the returned value is passed to Path.getChild so we must not return a full
-          // path here.
-          return PathFragment.create(WindowsFileOperations.getLongPath(path)).getBaseName();
-        } catch (IOException e) {
-          return null;
-        }
-      };
-
-  @VisibleForTesting
-  private enum WindowsPathFactory implements PathFactory {
-    INSTANCE {
-      @Override
-      public Path createRootPath(FileSystem filesystem) {
-        return new WindowsPath(filesystem, PathFragment.ROOT_DIR, null);
-      }
-
-      @Override
-      public Path createChildPath(Path parent, String childName) {
-        Preconditions.checkState(parent instanceof WindowsPath);
-        return new WindowsPath(parent.getFileSystem(), childName, (WindowsPath) parent);
-      }
-
-      @Override
-      public Path getCachedChildPathInternal(Path path, String childName) {
-        return WindowsPathFactory.getCachedChildPathInternalImpl(
-            path, childName, WINDOWS_SHORT_PATH_RESOLVER);
-      }
-    };
-
-    private static Path getCachedChildPathInternalImpl(
-        Path parent, String child, Function<String, String> resolver) {
-      if (parent != null && parent.isRootDirectory()) {
-        // This is a top-level directory. It must be a drive name ("C:" or "c").
-        if (WindowsPath.isWindowsVolumeName(child)) {
-          child = WindowsPath.getDriveLetter((WindowsPath) parent, child) + ":";
-        } else {
-          throw new IllegalArgumentException("Cannot create Unix-style paths on Windows.");
-        }
-      }
-
-      String resolvedChild = child;
-      if (parent != null && !parent.isRootDirectory() && SHORT_NAME_MATCHER.apply(child)) {
-        String pathString = parent.getPathString();
-        if (!pathString.endsWith("/")) {
-          pathString += "/";
-        }
-        pathString += child;
-        resolvedChild = resolver.apply(pathString);
-      }
-      return Path.getCachedChildPathInternal(
-          parent,
-          // If resolution succeeded, or we didn't attempt to resolve, then `resolvedChild` has the
-          // child name. If it's null, then resolution failed; use the unresolved child name in that
-          // case.
-          resolvedChild != null ? resolvedChild : child,
-          // If resolution failed, likely because the path doesn't exist, then do not cache the
-          // child. If we did, then in case the path later came into existence, we'd have a stale
-          // cache entry.
-          /* cacheable */ resolvedChild != null);
-    }
-
-    /**
-     * Creates a {@link PathFactory} with a mock shortname resolver.
-     *
-     * <p>The factory works exactly like the actual one ({@link WindowsPathFactory#INSTANCE}) except
-     * it's using the mock resolver.
-     */
-    public static PathFactory createForTesting(final Function<String, String> mockResolver) {
-      return new PathFactory() {
-        @Override
-        public Path createRootPath(FileSystem filesystem) {
-          return INSTANCE.createRootPath(filesystem);
-        }
-
-        @Override
-        public Path createChildPath(Path parent, String childName) {
-          return INSTANCE.createChildPath(parent, childName);
-        }
-
-        @Override
-        public Path getCachedChildPathInternal(Path path, String childName) {
-          return WindowsPathFactory.getCachedChildPathInternalImpl(path, childName, mockResolver);
-        }
-      };
-    }
-  }
-
-  /** A windows-specific subclass of Path. */
-  @VisibleForTesting
-  protected static final class WindowsPath extends Path {
-
-    // The drive letter is '\0' if and only if this Path is the filesystem root "/".
-    private char driveLetter;
-
-    private WindowsPath(FileSystem fileSystem) {
-      super(fileSystem);
-      this.driveLetter = '\0';
-    }
-
-    private WindowsPath(FileSystem fileSystem, String name, WindowsPath parent) {
-      super(fileSystem, name, parent);
-      this.driveLetter = getDriveLetter(parent, name);
-    }
-
-    @Override
-    protected void buildPathString(StringBuilder result) {
-      if (isRootDirectory()) {
-        result.append(PathFragment.ROOT_DIR);
-      } else {
-        if (isTopLevelDirectory()) {
-          result.append(driveLetter).append(':').append(PathFragment.SEPARATOR_CHAR);
-        } else {
-          WindowsPath parent = (WindowsPath) getParentDirectory();
-          parent.buildPathString(result);
-          if (!parent.isTopLevelDirectory()) {
-            result.append(PathFragment.SEPARATOR_CHAR);
-          }
-          result.append(getBaseName());
-        }
-      }
-    }
-
-    @Override
-    public void reinitializeAfterDeserialization() {
-      Preconditions.checkState(
-          getParentDirectory().isRootDirectory() || getParentDirectory() instanceof WindowsPath);
-      this.driveLetter =
-          (getParentDirectory() != null) ? ((WindowsPath) getParentDirectory()).driveLetter : '\0';
-    }
-
-    @Override
-    public boolean isMaybeRelativeTo(Path ancestorCandidate) {
-      Preconditions.checkState(ancestorCandidate instanceof WindowsPath);
-      return ancestorCandidate.isRootDirectory()
-          || driveLetter == ((WindowsPath) ancestorCandidate).driveLetter;
-    }
-
-    @Override
-    public boolean isTopLevelDirectory() {
-      return isRootDirectory() || getParentDirectory().isRootDirectory();
-    }
-
-    @Override
-    public PathFragment asFragment() {
-      String[] segments = getSegments();
-      if (segments.length > 0) {
-        // Strip off the first segment that contains the volume name.
-        segments = Arrays.copyOfRange(segments, 1, segments.length);
-      }
-
-      return PathFragment.create(driveLetter, true, segments);
-    }
-
-    @Override
-    protected Path getRootForRelativePathComputation(PathFragment relative) {
-      Path result = this;
-      if (relative.isAbsolute()) {
-        result = getFileSystem().getRootDirectory();
-        if (!relative.windowsVolume().isEmpty()) {
-          result = result.getRelative(relative.windowsVolume());
-        }
-      }
-      return result;
-    }
-
-    private static boolean isWindowsVolumeName(String name) {
-      return (name.length() == 1 || (name.length() == 2 && name.charAt(1) == ':'))
-          && Character.isLetter(name.charAt(0));
-    }
-
-    private static char getDriveLetter(WindowsPath parent, String name) {
-      if (parent == null) {
-        return '\0';
-      } else {
-        if (parent.isRootDirectory()) {
-          Preconditions.checkState(
-              isWindowsVolumeName(name),
-              "top-level directory on Windows must be a drive (name = '%s')",
-              name);
-          return Character.toUpperCase(name.charAt(0));
-        } else {
-          return parent.driveLetter;
-        }
-      }
-    }
-
-    @VisibleForTesting
-    @Override
-    protected synchronized void applyToChildren(Predicate<Path> function) {
-      super.applyToChildren(function);
-    }
-  }
-
-  @VisibleForTesting
-  static PathFactory getPathFactoryForTesting(Function<String, String> mockResolver) {
-    return WindowsPathFactory.createForTesting(mockResolver);
-  }
-
   public static final LinkOption[] NO_OPTIONS = new LinkOption[0];
   public static final LinkOption[] NO_FOLLOW = new LinkOption[] {LinkOption.NOFOLLOW_LINKS};
 
@@ -281,11 +41,6 @@
   }
 
   @Override
-  protected PathFactory getPathFactory() {
-    return WindowsPathFactory.INSTANCE;
-  }
-
-  @Override
   public String getFileSystemType(Path path) {
     // TODO(laszlocsomor): implement this properly, i.e. actually query this information from
     // somewhere (java.nio.Filesystem? System.getProperty? implement JNI method and use WinAPI?).
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
index a698840..af591cc 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsSubprocessFactory.java
@@ -83,9 +83,7 @@
     // If it's not absolute, then it cannot be longer than MAX_PATH, since MAX_PATH also limits the
     // length of file names.
     PathFragment argv0fragment = PathFragment.create(argv0);
-    return (argv0fragment.isAbsolute())
-        ? argv0fragment.normalize().getPathString().replace('/', '\\')
-        : argv0;
+    return (argv0fragment.isAbsolute()) ? argv0fragment.getPathString().replace('/', '\\') : argv0;
   }
 
   private String getRedirectPath(StreamAction action, File file) {
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD
index 7cbf304..538cb8f 100644
--- a/src/test/java/com/google/devtools/build/lib/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -9,8 +9,7 @@
 CROSS_PLATFORM_WINDOWS_TESTS = [
     "util/DependencySetWindowsTest.java",
     "vfs/PathFragmentWindowsTest.java",
-    "vfs/WindowsLocalPathTest.java",
-    "windows/PathWindowsTest.java",
+    "vfs/WindowsPathTest.java",
 ]
 
 # Tests for Windows-specific functionality that run on Windows.
@@ -18,7 +17,6 @@
     ["windows/*.java"],
     exclude = [
         "windows/MockSubprocess.java",
-        "windows/PathWindowsTest.java",
     ],
 )
 
@@ -374,6 +372,7 @@
     deps = [
         ":guava_junit_truth",
         ":vfs_filesystem_test",
+        "//src/main/java/com/google/devtools/build/lib:os_util",
         "//src/main/java/com/google/devtools/build/lib/vfs",
     ],
 )
@@ -401,7 +400,7 @@
 # systems
 java_test(
     name = "windows_test",
-    srcs = CROSS_PLATFORM_WINDOWS_TESTS + ["vfs/LocalPathAbstractTest.java"],
+    srcs = CROSS_PLATFORM_WINDOWS_TESTS + ["vfs/PathAbstractTest.java"],
     jvm_flags = [
         "-Dblaze.os=Windows",
         "-Dbazel.windows_unix_root=C:/fake/msys",
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
index 363bae9..f26d00e 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ArtifactTest.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.rules.java.JavaSemantics;
 import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodecAdapter;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -58,7 +59,7 @@
     Path f1 = scratch.file("/exec/dir/file.ext");
     Path bogusDir = scratch.file("/exec/dir/bogus");
     try {
-      new Artifact(f1, ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir));
+      new Artifact(ArtifactRoot.asDerivedRoot(execDir, bogusDir), f1.relativeTo(execDir));
       fail("Expected IllegalArgumentException constructing artifact with a bad root dir");
     } catch (IllegalArgumentException expected) {}
   }
@@ -93,7 +94,7 @@
   @Test
   public void testRootPrefixedExecPath_normal() throws IOException {
     Path f1 = scratch.file("/exec/root/dir/file.ext");
-    Artifact a1 = new Artifact(f1, rootDir, f1.relativeTo(execDir));
+    Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir));
     assertThat(Artifact.asRootPrefixedExecPath(a1)).isEqualTo("root:dir/file.ext");
   }
 
@@ -109,9 +110,10 @@
   public void testRootPrefixedExecPath_nullRootDir() throws IOException {
     Path f1 = scratch.file("/exec/dir/file.ext");
     try {
-      new Artifact(f1, null, f1.relativeTo(execDir));
-      fail("Expected IllegalArgumentException creating artifact with null root");
-    } catch (IllegalArgumentException expected) {}
+      new Artifact(null, f1.relativeTo(execDir));
+      fail("Expected NullPointerException creating artifact with null root");
+    } catch (NullPointerException expected) {
+    }
   }
 
   @Test
@@ -119,9 +121,9 @@
     Path f1 = scratch.file("/exec/root/dir/file1.ext");
     Path f2 = scratch.file("/exec/root/dir/dir/file2.ext");
     Path f3 = scratch.file("/exec/root/dir/dir/dir/file3.ext");
-    Artifact a1 = new Artifact(f1, rootDir, f1.relativeTo(execDir));
-    Artifact a2 = new Artifact(f2, rootDir, f2.relativeTo(execDir));
-    Artifact a3 = new Artifact(f3, rootDir, f3.relativeTo(execDir));
+    Artifact a1 = new Artifact(rootDir, f1.relativeTo(execDir));
+    Artifact a2 = new Artifact(rootDir, f2.relativeTo(execDir));
+    Artifact a3 = new Artifact(rootDir, f3.relativeTo(execDir));
     List<String> strings = new ArrayList<>();
     Artifact.addRootPrefixedExecPaths(Lists.newArrayList(a1, a2, a3), strings);
     assertThat(strings).containsExactly(
@@ -283,10 +285,9 @@
 
   @Test
   public void testToDetailString() throws Exception {
-    Path execRoot = scratch.getFileSystem().getPath("/");
+    Path execRoot = scratch.getFileSystem().getPath("/a");
     Artifact a =
         new Artifact(
-            scratch.file("/a/b/c"),
             ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a/b")),
             PathFragment.create("b/c"));
     assertThat(a.toDetailString()).isEqualTo("[[/a]b]c");
@@ -294,17 +295,12 @@
 
   @Test
   public void testWeirdArtifact() throws Exception {
-    try {
-      Path execRoot = scratch.getFileSystem().getPath("/");
-      new Artifact(
-          scratch.file("/a/b/c"),
-          ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a")),
-          PathFragment.create("c"));
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessage(
-          "c: illegal execPath doesn't end with b/c at /a/b/c with root /a[derived]");
-    }
+    Path execRoot = scratch.getFileSystem().getPath("/");
+    MoreAsserts.expectThrows(
+        IllegalArgumentException.class,
+        () ->
+            new Artifact(
+                ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/a")), PathFragment.create("c")));
   }
 
   @Test
@@ -319,25 +315,23 @@
 
   @Test
   public void testSerializeToStringWithExecPath() throws Exception {
-    Path execRoot = scratch.getFileSystem().getPath("/");
-    Path path = scratch.file("/aaa/bbb/ccc");
+    Path execRoot = scratch.getFileSystem().getPath("/aaa");
     ArtifactRoot root = ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aaa/bbb"));
     PathFragment execPath = PathFragment.create("bbb/ccc");
 
-    assertThat(new Artifact(path, root, execPath).serializeToString()).isEqualTo("bbb/ccc /3");
+    assertThat(new Artifact(root, execPath).serializeToString()).isEqualTo("bbb/ccc /3");
   }
 
   @Test
   public void testSerializeToStringWithOwner() throws Exception {
-    Path execRoot = scratch.getFileSystem().getPath("/");
+    Path execRoot = scratch.getFileSystem().getPath("/aa");
     assertThat(
             new Artifact(
-                    scratch.file("/aa/b/c"),
-                    ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aa")),
+                    ArtifactRoot.asDerivedRoot(execRoot, scratch.dir("/aa/b")),
                     PathFragment.create("b/c"),
                     new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar")))
                 .serializeToString())
-        .isEqualTo("b/c /3 //foo:bar");
+        .isEqualTo("b/c /1 //foo:bar");
   }
 
   @Test
@@ -349,10 +343,9 @@
             new Artifact(
                 PathFragment.create("src/b"), ArtifactRoot.asSourceRoot(Root.fromPath(execDir))),
             new Artifact(
-                scratch.file("/src/c"),
                 ArtifactRoot.asDerivedRoot(
                     scratch.getFileSystem().getPath("/"), scratch.dir("/src")),
-                PathFragment.create("c"),
+                PathFragment.create("src/c"),
                 new LabelArtifactOwner(Label.parseAbsoluteUnchecked("//foo:bar"))))
         .buildAndRunTests();
   }
@@ -387,7 +380,6 @@
   public void testIsSourceArtifact() throws Exception {
     assertThat(
             new Artifact(
-                    scratch.file("/src/foo.cc"),
                     ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/"))),
                     PathFragment.create("src/foo.cc"))
                 .isSourceArtifact())
diff --git a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
index 085415cf..b9f341a 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/CustomCommandLineTest.java
@@ -984,7 +984,6 @@
   private SpecialArtifact createTreeArtifact(String rootRelativePath) {
     PathFragment relpath = PathFragment.create(rootRelativePath);
     return new SpecialArtifact(
-        rootDir.getRoot().getRelative(relpath),
         rootDir,
         rootDir.getExecPath().getRelative(relpath),
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java
index 4c9d2dd..8678d50 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/ParamFileWriteActionTest.java
@@ -111,7 +111,6 @@
   private SpecialArtifact createTreeArtifact(String rootRelativePath) {
     PathFragment relpath = PathFragment.create(rootRelativePath);
     return new SpecialArtifact(
-        rootDir.getRoot().getRelative(relpath),
         rootDir,
         rootDir.getExecPath().getRelative(relpath),
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java
index 345aa97..1ce4fd5 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/PopulateTreeArtifactActionTest.java
@@ -346,7 +346,6 @@
   private SpecialArtifact createTreeArtifact(String rootRelativePath) {
     PathFragment relpath = PathFragment.create(rootRelativePath);
     return new SpecialArtifact(
-        root.getRoot().getRelative(relpath),
         root,
         root.getExecPath().getRelative(relpath),
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
index ad03fe7..17588a6 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/actions/SpawnActionTemplateTest.java
@@ -325,7 +325,6 @@
   private SpecialArtifact createTreeArtifact(String rootRelativePath) {
     PathFragment relpath = PathFragment.create(rootRelativePath);
     return new SpecialArtifact(
-        root.getRoot().getRelative(relpath),
         root,
         root.getExecPath().getRelative(relpath),
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
index ca8bef9..5e22269 100644
--- a/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildeventstream/transports/BuildEventTransportFactoryTest.java
@@ -133,6 +133,13 @@
     assertThat(transports).isEmpty();
   }
 
+  @Test
+  public void testPathToUriString() {
+    // See https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/
+    assertThat(BuildEventTransportFactory.pathToUriString("C:/Temp/Foo Bar.txt"))
+        .isEqualTo("file:///C:/Temp/Foo%20Bar.txt");
+  }
+
   private void sendEventsAndClose(BuildEvent event, Iterable<BuildEventTransport> transports)
       throws IOException{
     for (BuildEventTransport transport : transports) {
diff --git a/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java b/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java
index 1cac353..a9b224f 100644
--- a/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/pkgcache/PathPackageLocatorTest.java
@@ -338,7 +338,7 @@
         workspace.getRelative("foo"),
         BazelSkyframeExecutorConstants.BUILD_FILES_BY_PRIORITY);
     assertThat(eventCollector.count()).isSameAs(1);
-    assertContainsEvent("The package path element './foo' will be taken relative");
+    assertContainsEvent("The package path element 'foo' will be taken relative");
   }
 
   /** Regression test for bug: "IllegalArgumentException in PathPackageLocator.create()" */
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
index 1bd5bed..9ea174a 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/ResourceTestBase.java
@@ -183,6 +183,6 @@
   public Artifact getResource(String pathString) {
     Path path = fileSystem.getPath("/" + RESOURCE_ROOT + "/" + pathString);
     return new Artifact(
-        path, root, root.getExecPath().getRelative(root.getRoot().relativize(path)), OWNER);
+        root, root.getExecPath().getRelative(root.getRoot().relativize(path)), OWNER);
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
index 4d7c3fc..36fcadf 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CcLibraryConfiguredTargetTest.java
@@ -1066,7 +1066,7 @@
     checkError(
         "root",
         "a",
-        "The include path 'd/../../somewhere' references a path outside of the execution root.",
+        "The include path '../somewhere' references a path outside of the execution root.",
         "cc_library(name='a', srcs=['a.cc'], copts=['-Id/../../somewhere'])");
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
index db56bd5..f7bf8eb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppLinkActionTest.java
@@ -524,10 +524,6 @@
 
   public Artifact getOutputArtifact(String relpath) {
     return new Artifact(
-        getTargetConfiguration()
-            .getBinDirectory(RepositoryName.MAIN)
-            .getRoot()
-            .getRelative(relpath),
         getTargetConfiguration().getBinDirectory(RepositoryName.MAIN),
         getTargetConfiguration().getBinFragment().getRelative(relpath));
   }
@@ -672,9 +668,7 @@
     FileSystem fs = scratch.getFileSystem();
     Path execRoot = fs.getPath(TestUtils.tmpDir());
     PathFragment execPath = PathFragment.create("out").getRelative(name);
-    Path path = execRoot.getRelative(execPath);
     return new SpecialArtifact(
-        path,
         ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")),
         execPath,
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java
index 4a3714b..e45e32f 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CrosstoolConfigurationLoaderTest.java
@@ -186,7 +186,7 @@
     assertThat(ccProvider.getTargetCpu()).isEqualTo("piii");
     assertThat(ccProvider.getTargetGnuSystemName()).isEqualTo("target-system-name");
 
-    assertThat(toolchain.getToolPathFragment(Tool.AR)).isEqualTo(getToolPath("/path-to-ar"));
+    assertThat(toolchain.getToolPathFragment(Tool.AR)).isEqualTo(getToolPath("path-to-ar"));
 
     assertThat(ccProvider.getAbi()).isEqualTo("abi-version");
     assertThat(ccProvider.getAbiGlibcVersion()).isEqualTo("abi-libc-version");
@@ -199,7 +199,7 @@
     assertThat(ccProvider.supportsFission()).isTrue();
 
     assertThat(ccProvider.getBuiltInIncludeDirectories())
-        .containsExactly(getToolPath("/system-include-dir"));
+        .containsExactly(getToolPath("system-include-dir"));
     assertThat(ccProvider.getSysroot()).isNull();
 
     assertThat(CppHelper.getCompilerOptions(toolchain, ccProvider, NO_FEATURES))
@@ -238,8 +238,8 @@
                     "CC_FLAGS", "")
                 .entrySet());
 
-    assertThat(toolchain.getToolPathFragment(Tool.LD)).isEqualTo(getToolPath("/path-to-ld"));
-    assertThat(toolchain.getToolPathFragment(Tool.DWP)).isEqualTo(getToolPath("/path-to-dwp"));
+    assertThat(toolchain.getToolPathFragment(Tool.LD)).isEqualTo(getToolPath("path-to-ld"));
+    assertThat(toolchain.getToolPathFragment(Tool.DWP)).isEqualTo(getToolPath("path-to-dwp"));
   }
 
   /**
@@ -605,7 +605,7 @@
                 .entrySet());
     assertThat(ccProviderA.getBuiltInIncludeDirectories())
         .containsExactly(
-            getToolPath("/system-include-dir-A-1"), getToolPath("/system-include-dir-A-2"))
+            getToolPath("system-include-dir-A-1"), getToolPath("system-include-dir-A-2"))
         .inOrder();
     assertThat(ccProviderA.getSysroot()).isEqualTo(PathFragment.create("some"));
 
@@ -682,9 +682,7 @@
     PackageIdentifier packageIdentifier =
         PackageIdentifier.create(
             TestConstants.TOOLS_REPOSITORY,
-            PathFragment.create(
-                PathFragment.create(TestConstants.MOCK_CC_CROSSTOOL_PATH),
-                PathFragment.create(path)));
+            PathFragment.create(TestConstants.MOCK_CC_CROSSTOOL_PATH).getRelative(path));
     return packageIdentifier.getPathUnderExecRoot();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
index cdd9d72..ee9b9fb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/HeaderThinningTest.java
@@ -189,7 +189,6 @@
   private Artifact getTreeArtifact(String name) {
     Artifact treeArtifactBase = getSourceArtifact(name);
     return new SpecialArtifact(
-        treeArtifactBase.getPath(),
         treeArtifactBase.getRoot(),
         treeArtifactBase.getExecPath(),
         treeArtifactBase.getArtifactOwner(),
diff --git a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
index d21acf1..bc0ebaa 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/proto/ProtoCompileActionBuilderTest.java
@@ -347,7 +347,6 @@
 
   private Artifact artifact(String ownerLabel, String path) {
     return new Artifact(
-        root.getRoot().getRelative(path),
         root,
         root.getExecPath().getRelative(path),
         new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel)));
@@ -356,7 +355,6 @@
   /** Creates a dummy artifact with the given path, that actually resides in /out/<path>. */
   private Artifact derivedArtifact(String ownerLabel, String path) {
     return new Artifact(
-        derivedRoot.getRoot().getRelative(path),
         derivedRoot,
         derivedRoot.getExecPath().getRelative(path),
         new LabelArtifactOwner(Label.parseAbsoluteUnchecked(ownerLabel)));
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java
index 7c86478..f8b6a9d 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionTemplateExpansionFunctionTest.java
@@ -39,7 +39,6 @@
 import com.google.devtools.build.lib.events.NullEventHandler;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.testutil.FoundationTestCase;
-import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.skyframe.EvaluationResult;
@@ -205,9 +204,7 @@
 
   private SpecialArtifact createTreeArtifact(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
-    Path fullPath = rootDirectory.getRelative(execPath);
     return new SpecialArtifact(
-        fullPath,
         ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")),
         execPath,
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
index 1699fc2..21b0a10 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ArtifactFunctionTest.java
@@ -250,10 +250,8 @@
 
   private Artifact createDerivedArtifact(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
-    Path fullPath = root.getRelative(execPath);
     Artifact output =
         new Artifact(
-            fullPath,
             ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
             execPath,
             ALL_OWNER);
@@ -264,9 +262,7 @@
   private Artifact createMiddlemanArtifact(String path) {
     ArtifactRoot middlemanRoot =
         ArtifactRoot.middlemanRoot(middlemanPath, middlemanPath.getRelative("out"));
-    Path fullPath = middlemanRoot.getRoot().getRelative(path);
-    return new Artifact(
-        fullPath, middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER);
+    return new Artifact(middlemanRoot, middlemanRoot.getExecPath().getRelative(path), ALL_OWNER);
   }
 
   private SpecialArtifact createDerivedTreeArtifactWithAction(String path) {
@@ -277,9 +273,7 @@
 
   private SpecialArtifact createDerivedTreeArtifactOnly(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
-    Path fullPath = root.getRelative(execPath);
     return new SpecialArtifact(
-        fullPath,
         ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
         execPath,
         ALL_OWNER,
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
index f7a989a..48ccd7e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/FilesystemValueCheckerTest.java
@@ -620,7 +620,6 @@
     outputDir.createDirectory();
     ArtifactRoot derivedRoot = ArtifactRoot.asDerivedRoot(fs.getPath("/"), outputDir);
     return new SpecialArtifact(
-        outputPath,
         derivedRoot,
         derivedRoot.getExecPath().getRelative(derivedRoot.getRoot().relativize(outputPath)),
         ArtifactOwner.NullArtifactOwner.INSTANCE,
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
index 872de47..25b510e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunctionTest.java
@@ -171,10 +171,8 @@
 
   private Artifact derivedArtifact(String path) {
     PathFragment execPath = PathFragment.create("out").getRelative(path);
-    Path fullPath = rootDirectory.getRelative(execPath);
     Artifact output =
         new Artifact(
-            fullPath,
             ArtifactRoot.asDerivedRoot(rootDirectory, rootDirectory.getRelative("out")),
             execPath);
     return output;
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
index 3b39d03..e63d1ea 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TimestampBuilderTestCase.java
@@ -336,9 +336,7 @@
   Artifact createDerivedArtifact(FileSystem fs, String name) {
     Path execRoot = fs.getPath(TestUtils.tmpDir());
     PathFragment execPath = PathFragment.create("out").getRelative(name);
-    Path path = execRoot.getRelative(execPath);
     return new Artifact(
-        path,
         ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")),
         execPath,
         ACTION_LOOKUP_KEY);
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
index 30b6c4c..5d24b9e 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactBuildTest.java
@@ -1192,9 +1192,7 @@
     FileSystem fs = scratch.getFileSystem();
     Path execRoot = fs.getPath(TestUtils.tmpDir());
     PathFragment execPath = PathFragment.create("out").getRelative(name);
-    Path path = execRoot.getRelative(execPath);
     return new SpecialArtifact(
-        path,
         ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out")),
         execPath,
         ACTION_LOOKUP_KEY,
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
index 96bc4be..9e3fd39 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/TreeArtifactMetadataTest.java
@@ -204,7 +204,6 @@
     Path fullPath = root.getRelative(execPath);
     SpecialArtifact output =
         new SpecialArtifact(
-            fullPath,
             ArtifactRoot.asDerivedRoot(root, root.getRelative("out")),
             execPath,
             ALL_OWNER,
diff --git a/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java b/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java
index d77b10f..2d81e13 100644
--- a/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java
+++ b/src/test/java/com/google/devtools/build/lib/unix/UnixPathEqualityTest.java
@@ -14,9 +14,9 @@
 package com.google.devtools.build.lib.unix;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
 
 import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
 import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.Path;
 import org.junit.Before;
@@ -93,25 +93,8 @@
     Path a = unixFs.getPath("/a");
     Path b = otherUnixFs.getPath("/b");
 
-    try {
-      a.renameTo(b);
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessageThat().contains("different filesystems");
-    }
-
-    try {
-      a.relativeTo(b);
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessageThat().contains("different filesystems");
-    }
-
-    try {
-      a.createSymbolicLink(b);
-      fail();
-    } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessageThat().contains("different filesystems");
-    }
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.renameTo(b));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.relativeTo(b));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> a.createSymbolicLink(b));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
index e8896bb..47ae76a 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemTest.java
@@ -14,7 +14,6 @@
 package com.google.devtools.build.lib.vfs;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.fail;
 
@@ -789,23 +788,6 @@
     assertThat(xNonEmptyDirectoryFoo.isFile()).isTrue();
   }
 
-  @Test
-  public void testCannotRemoveRoot() {
-    Path rootDirectory = testFS.getRootDirectory();
-    try {
-      rootDirectory.delete();
-      fail();
-    } catch (IOException e) {
-      String msg = e.getMessage();
-      assertWithMessage(String.format("got %s want EBUSY or ENOTEMPTY", msg))
-          .that(
-              msg.endsWith(" (Directory not empty)")
-                  || msg.endsWith(" (Device or resource busy)")
-                  || msg.endsWith(" (Is a directory)"))
-          .isTrue(); // Happens on OS X.
-    }
-  }
-
   // Test the date functions
   @Test
   public void testCreateFileChangesTimeOfDirectory() throws Exception {
@@ -1105,22 +1087,13 @@
   // Test the Paths
   @Test
   public void testGetPathOnlyAcceptsAbsolutePath() {
-    try {
-      testFS.getPath("not-absolute");
-      fail("The expected Exception was not thrown.");
-    } catch (IllegalArgumentException ex) {
-      assertThat(ex).hasMessage("not-absolute (not an absolute path)");
-    }
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> testFS.getPath("not-absolute"));
   }
 
   @Test
   public void testGetPathOnlyAcceptsAbsolutePathFragment() {
-    try {
-      testFS.getPath(PathFragment.create("not-absolute"));
-      fail("The expected Exception was not thrown.");
-    } catch (IllegalArgumentException ex) {
-      assertThat(ex).hasMessage("not-absolute (not an absolute path)");
-    }
+    MoreAsserts.expectThrows(
+        IllegalArgumentException.class, () -> testFS.getPath(PathFragment.create("not-absolute")));
   }
 
   // Test the access permissions
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
index 6563633..f9cc1a9 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/FileSystemUtilsTest.java
@@ -175,11 +175,29 @@
   @Test
   public void testRelativePath() throws IOException {
     createTestDirectoryTree();
-    assertThat(relativePath(topDir, file1).getPathString()).isEqualTo("file-1");
-    assertThat(relativePath(topDir, topDir).getPathString()).isEqualTo(".");
-    assertThat(relativePath(topDir, dirLink).getPathString()).isEqualTo("a-dir/inner-dir/dir-link");
-    assertThat(relativePath(topDir, file4).getPathString()).isEqualTo("../file-4");
-    assertThat(relativePath(innerDir, file4).getPathString()).isEqualTo("../../../file-4");
+    assertThat(
+            relativePath(PathFragment.create("/top-dir"), PathFragment.create("/top-dir/file-1"))
+                .getPathString())
+        .isEqualTo("file-1");
+    assertThat(
+            relativePath(PathFragment.create("/top-dir"), PathFragment.create("/top-dir"))
+                .getPathString())
+        .isEqualTo("");
+    assertThat(
+            relativePath(
+                    PathFragment.create("/top-dir"),
+                    PathFragment.create("/top-dir/a-dir/inner-dir/dir-link"))
+                .getPathString())
+        .isEqualTo("a-dir/inner-dir/dir-link");
+    assertThat(
+            relativePath(PathFragment.create("/top-dir"), PathFragment.create("/file-4"))
+                .getPathString())
+        .isEqualTo("../file-4");
+    assertThat(
+            relativePath(
+                    PathFragment.create("/top-dir/a-dir/inner-dir"), PathFragment.create("/file-4"))
+                .getPathString())
+        .isEqualTo("../../../file-4");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java b/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java
deleted file mode 100644
index 9386952..0000000
--- a/src/test/java/com/google/devtools/build/lib/vfs/LocalPathAbstractTest.java
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.collect.Lists;
-import com.google.common.testing.EqualsTester;
-import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Test;
-
-/** Tests for {@link LocalPath}. */
-public abstract class LocalPathAbstractTest {
-
-  private OsPathPolicy os;
-
-  @Before
-  public void setup() {
-    os = getFilePathOs();
-  }
-
-  @Test
-  public void testEqualsAndHashCode() {
-    new EqualsTester()
-        .addEqualityGroup(
-            create("../relative/path"), create("..").getRelative("relative").getRelative("path"))
-        .addEqualityGroup(create("something/else"))
-        .addEqualityGroup(create(""), LocalPath.EMPTY)
-        .testEquals();
-  }
-
-  @Test
-  public void testRelativeTo() {
-    assertThat(create("").relativeTo(create("")).getPathString()).isEmpty();
-    assertThat(create("foo").relativeTo(create("foo")).getPathString()).isEmpty();
-    assertThat(create("foo/bar/baz").relativeTo(create("foo")).getPathString())
-        .isEqualTo("bar/baz");
-    assertThat(create("foo/bar/baz").relativeTo(create("foo/bar")).getPathString())
-        .isEqualTo("baz");
-    assertThat(create("foo").relativeTo(create("")).getPathString()).isEqualTo("foo");
-
-    // Cannot relativize non-ancestors
-    assertThrows(IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("fo")));
-
-    // Make sure partial directory matches aren't reported
-    assertThrows(
-        IllegalArgumentException.class, () -> create("foo/bar").relativeTo(create("foo/ba")));
-  }
-
-  @Test
-  public void testGetRelative() {
-    assertThat(create("a").getRelative("b").getPathString()).isEqualTo("a/b");
-    assertThat(create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d");
-    assertThat(create("a").getRelative("").getPathString()).isEqualTo("a");
-    assertThat(create("a/b").getRelative("../c").getPathString()).isEqualTo("a/c");
-    assertThat(create("a/b").getRelative("..").getPathString()).isEqualTo("a");
-  }
-
-  @Test
-  public void testEmptyPathToEmptyPath() {
-    // compare string forms
-    assertThat(create("").getPathString()).isEmpty();
-    // compare fragment forms
-    assertThat(create("")).isEqualTo(create(""));
-  }
-
-  @Test
-  public void testSimpleNameToSimpleName() {
-    // compare string forms
-    assertThat(create("foo").getPathString()).isEqualTo("foo");
-    // compare fragment forms
-    assertThat(create("foo")).isEqualTo(create("foo"));
-  }
-
-  @Test
-  public void testSimplePathToSimplePath() {
-    // compare string forms
-    assertThat(create("foo/bar").getPathString()).isEqualTo("foo/bar");
-    // compare fragment forms
-    assertThat(create("foo/bar")).isEqualTo(create("foo/bar"));
-  }
-
-  @Test
-  public void testStripsTrailingSlash() {
-    // compare string forms
-    assertThat(create("foo/bar/").getPathString()).isEqualTo("foo/bar");
-    // compare fragment forms
-    assertThat(create("foo/bar/")).isEqualTo(create("foo/bar"));
-  }
-
-  @Test
-  public void testGetParentDirectory() {
-    LocalPath fooBarWiz = create("foo/bar/wiz");
-    LocalPath fooBar = create("foo/bar");
-    LocalPath foo = create("foo");
-    LocalPath empty = create("");
-    assertThat(fooBarWiz.getParentDirectory()).isEqualTo(fooBar);
-    assertThat(fooBar.getParentDirectory()).isEqualTo(foo);
-    assertThat(foo.getParentDirectory()).isEqualTo(empty);
-    assertThat(empty.getParentDirectory()).isNull();
-  }
-
-  @Test
-  public void testBasename() throws Exception {
-    assertThat(create("foo/bar").getBaseName()).isEqualTo("bar");
-    assertThat(create("foo/").getBaseName()).isEqualTo("foo");
-    assertThat(create("foo").getBaseName()).isEqualTo("foo");
-    assertThat(create("").getBaseName()).isEmpty();
-  }
-
-  @Test
-  public void testStartsWith() {
-    // (relative path, relative prefix) => true
-    assertThat(create("foo/bar").startsWith(create("foo/bar"))).isTrue();
-    assertThat(create("foo/bar").startsWith(create("foo"))).isTrue();
-    assertThat(create("foot/bar").startsWith(create("foo"))).isFalse();
-  }
-
-  @Test
-  public void testNormalize() {
-    assertThat(create("a/b")).isEqualTo(create("a/b"));
-    assertThat(create("a/../../b")).isEqualTo(create("../b"));
-    assertThat(create("a/../..")).isEqualTo(create(".."));
-    assertThat(create("a/../b")).isEqualTo(create("b"));
-    assertThat(create("a/b/../b")).isEqualTo(create("a/b"));
-  }
-
-  @Test
-  public void testNormalStringsDoNotAllocate() {
-    String normal1 = "a/b/hello.txt";
-    assertThat(create(normal1).getPathString()).isSameAs(normal1);
-
-    // Sanity check our testing strategy
-    String notNormal = "a/../b";
-    assertThat(create(notNormal).getPathString()).isNotSameAs(notNormal);
-  }
-
-  @Test
-  public void testComparableSortOrder() {
-    List<LocalPath> list =
-        Lists.newArrayList(
-            create("zzz"),
-            create("ZZZ"),
-            create("ABC"),
-            create("aBc"),
-            create("AbC"),
-            create("abc"));
-    Collections.sort(list);
-    List<String> result = list.stream().map(LocalPath::getPathString).collect(toList());
-
-    if (os.isCaseSensitive()) {
-      assertThat(result).containsExactly("ABC", "AbC", "ZZZ", "aBc", "abc", "zzz").inOrder();
-    } else {
-      // Partial ordering among case-insensitive items guaranteed by Collections.sort stability
-      assertThat(result).containsExactly("ABC", "aBc", "AbC", "abc", "zzz", "ZZZ").inOrder();
-    }
-  }
-
-  protected abstract OsPathPolicy getFilePathOs();
-
-  protected LocalPath create(String path) {
-    return LocalPath.createWithOs(path, os);
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java
deleted file mode 100644
index c99ad48..0000000
--- a/src/test/java/com/google/devtools/build/lib/vfs/MacOsLocalPathTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.vfs;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.testing.EqualsTester;
-import com.google.devtools.build.lib.vfs.LocalPath.MacOsPathPolicy;
-import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests Mac-specific parts of {@link LocalPath}. */
-@RunWith(JUnit4.class)
-public class MacOsLocalPathTest extends UnixLocalPathTest {
-
-  @Override
-  protected OsPathPolicy getFilePathOs() {
-    return new MacOsPathPolicy();
-  }
-
-  @Test
-  public void testMacEqualsAndHashCode() {
-    new EqualsTester()
-        .addEqualityGroup(create("a/b"), create("A/B"))
-        .addEqualityGroup(create("/a/b"), create("/A/B"))
-        .addEqualityGroup(create("something/else"))
-        .addEqualityGroup(create("/something/else"))
-        .testEquals();
-  }
-
-  @Test
-  public void testCaseIsPreserved() {
-    assertThat(create("a/B").getPathString()).isEqualTo("a/B");
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
index 6d11285..5dc43a2 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/NativePathTest.java
@@ -106,13 +106,6 @@
   }
 
   @Test
-  public void testParentOfRootIsRoot() {
-    assertThat(fs.getPath("/..")).isEqualTo(fs.getPath("/"));
-    assertThat(fs.getPath("/../../../../../..")).isEqualTo(fs.getPath("/"));
-    assertThat(fs.getPath("/../../../foo")).isEqualTo(fs.getPath("/foo"));
-  }
-
-  @Test
   public void testIsDirectory() {
     assertThat(fs.getPath(aDirectory.getPath()).isDirectory()).isTrue();
     assertThat(fs.getPath(aFile.getPath()).isDirectory()).isFalse();
@@ -241,14 +234,4 @@
     assertThat(in.read()).isEqualTo(-1);
     in.close();
   }
-
-  @Test
-  public void testDerivedSegmentEquality() {
-    Path absoluteSegment = fs.getRootDirectory();
-
-    Path derivedNode = absoluteSegment.getChild("derivedSegment");
-    Path otherDerivedNode = absoluteSegment.getChild("derivedSegment");
-
-    assertThat(otherDerivedNode).isSameAs(derivedNode);
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java
new file mode 100644
index 0000000..7494683
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathAbstractTest.java
@@ -0,0 +1,141 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.vfs;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import com.google.devtools.build.lib.clock.BlazeClock;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link Path}. */
+public abstract class PathAbstractTest {
+
+  private FileSystem fileSystem;
+  private boolean isCaseSensitive;
+
+  @Before
+  public void setup() {
+    fileSystem = new InMemoryFileSystem(BlazeClock.instance());
+    isCaseSensitive = OsPathPolicy.getFilePathOs().isCaseSensitive();
+  }
+
+  @Test
+  public void testStripsTrailingSlash() {
+    // compare string forms
+    assertThat(create("/foo/bar/").getPathString()).isEqualTo("/foo/bar");
+    // compare fragment forms
+    assertThat(create("/foo/bar/")).isEqualTo(create("/foo/bar"));
+  }
+
+  @Test
+  public void testBasename() throws Exception {
+    assertThat(create("/foo/bar").getBaseName()).isEqualTo("bar");
+    assertThat(create("/foo/").getBaseName()).isEqualTo("foo");
+    assertThat(create("/foo").getBaseName()).isEqualTo("foo");
+    assertThat(create("/").getBaseName()).isEmpty();
+  }
+
+  @Test
+  public void testNormalStringsDoNotAllocate() {
+    String normal1 = "/a/b/hello.txt";
+    assertThat(create(normal1).getPathString()).isSameAs(normal1);
+
+    // Sanity check our testing strategy
+    String notNormal = "/a/../b";
+    assertThat(create(notNormal).getPathString()).isNotSameAs(notNormal);
+  }
+
+  @Test
+  public void testComparableSortOrder() {
+    List<Path> list =
+        Lists.newArrayList(
+            create("/zzz"),
+            create("/ZZZ"),
+            create("/ABC"),
+            create("/aBc"),
+            create("/AbC"),
+            create("/abc"));
+    Collections.sort(list);
+    List<String> result = list.stream().map(Path::getPathString).collect(toList());
+
+    if (isCaseSensitive) {
+      assertThat(result).containsExactly("/ABC", "/AbC", "/ZZZ", "/aBc", "/abc", "/zzz").inOrder();
+    } else {
+      // Partial ordering among case-insensitive items guaranteed by Collections.sort stability
+      assertThat(result).containsExactly("/ABC", "/aBc", "/AbC", "/abc", "/zzz", "/ZZZ").inOrder();
+    }
+  }
+
+  @Test
+  public void testSerialization() throws Exception {
+    FileSystem oldFileSystem = Path.getFileSystemForSerialization();
+    try {
+      Path.setFileSystemForSerialization(fileSystem);
+      Path root = fileSystem.getPath("/");
+      Path p1 = fileSystem.getPath("/foo");
+      Path p2 = fileSystem.getPath("/foo/bar");
+
+      ByteArrayOutputStream bos = new ByteArrayOutputStream();
+      ObjectOutputStream oos = new ObjectOutputStream(bos);
+
+      oos.writeObject(root);
+      oos.writeObject(p1);
+      oos.writeObject(p2);
+
+      ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+      ObjectInputStream ois = new ObjectInputStream(bis);
+
+      Path dsRoot = (Path) ois.readObject();
+      Path dsP1 = (Path) ois.readObject();
+      Path dsP2 = (Path) ois.readObject();
+
+      new EqualsTester()
+          .addEqualityGroup(root, dsRoot)
+          .addEqualityGroup(p1, dsP1)
+          .addEqualityGroup(p2, dsP2)
+          .testEquals();
+
+      assertThat(p2.startsWith(p1)).isTrue();
+      assertThat(p2.startsWith(dsP1)).isTrue();
+      assertThat(dsP2.startsWith(p1)).isTrue();
+      assertThat(dsP2.startsWith(dsP1)).isTrue();
+
+      // Regression test for a very specific bug in compareTo involving our incorrect usage of
+      // reference equality rather than logical equality.
+      String relativePathStringA = "child/grandchildA";
+      String relativePathStringB = "child/grandchildB";
+      assertThat(
+              p1.getRelative(relativePathStringA).compareTo(dsP1.getRelative(relativePathStringB)))
+          .isEqualTo(
+              p1.getRelative(relativePathStringA).compareTo(p1.getRelative(relativePathStringB)));
+    } finally {
+      Path.setFileSystemForSerialization(oldFileSystem);
+    }
+  }
+
+  protected Path create(String path) {
+    return Path.create(path, fileSystem);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
index 7fa3c73..02446c0 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentTest.java
@@ -15,13 +15,14 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
+import static com.google.devtools.build.lib.vfs.PathFragment.create;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.testing.EqualsTester;
 import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester;
+import com.google.devtools.build.lib.testutil.MoreAsserts;
 import com.google.devtools.build.lib.testutil.TestUtils;
 import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
 import java.io.File;
@@ -36,32 +37,6 @@
  */
 @RunWith(JUnit4.class)
 public class PathFragmentTest {
-  @Test
-  public void testMergeFourPathsWithAbsolute() {
-    assertThat(
-            PathFragment.create(
-                PathFragment.create("x/y"),
-                PathFragment.create("z/a"),
-                PathFragment.create("/b/c"),
-                PathFragment.create("d/e")))
-        .isEqualTo(PathFragment.create("x/y/z/a/b/c/d/e"));
-  }
-
-  @Test
-  public void testCreateInternsPathFragments() {
-    String[] firstSegments = new String[] {"hello", "world"};
-    PathFragment first = PathFragment.create(
-        /*driveLetter=*/ '\0', /*isAbsolute=*/ false, firstSegments);
-
-    String[] secondSegments = new String[] {new String("hello"), new String("world")};
-    PathFragment second = PathFragment.create(
-        /*driveLetter=*/ '\0', /*isAbsolute=*/ false, secondSegments);
-
-    assertThat(first.segmentCount()).isEqualTo(second.segmentCount());
-    for (int i = 0; i < first.segmentCount(); i++) {
-      assertThat(first.getSegment(i)).isSameAs(second.getSegment(i));
-    }
-  }
 
   @Test
   public void testEqualsAndHashCode() {
@@ -69,23 +44,21 @@
 
     new EqualsTester()
         .addEqualityGroup(
-            PathFragment.create("../relative/path"),
-            PathFragment.create("..").getRelative("relative").getRelative("path"),
-            PathFragment.createAlreadyInterned(
-                '\0', false, new String[] {"..", "relative", "path"}),
-            PathFragment.create(new File("../relative/path")))
-        .addEqualityGroup(PathFragment.create("something/else"))
-        .addEqualityGroup(PathFragment.create("/something/else"))
-        .addEqualityGroup(PathFragment.create("/"), PathFragment.create("//////"))
-        .addEqualityGroup(PathFragment.create(""), PathFragment.EMPTY_FRAGMENT)
+            create("../relative/path"),
+            create("..").getRelative("relative").getRelative("path"),
+            create(new File("../relative/path").getPath()))
+        .addEqualityGroup(create("something/else"))
+        .addEqualityGroup(create("/something/else"))
+        .addEqualityGroup(create("/"), create("//////"))
+        .addEqualityGroup(create(""), PathFragment.EMPTY_FRAGMENT)
         .addEqualityGroup(filesystem.getPath("/")) // A Path object.
         .testEquals();
   }
 
   @Test
   public void testHashCodeCache() {
-    PathFragment relativePath = PathFragment.create("../relative/path");
-    PathFragment rootPath = PathFragment.create("/");
+    PathFragment relativePath = create("../relative/path");
+    PathFragment rootPath = create("/");
 
     int oldResult = relativePath.hashCode();
     int rootResult = rootPath.hashCode();
@@ -93,279 +66,272 @@
     assertThat(rootPath.hashCode()).isEqualTo(rootResult);
   }
 
-  private void checkRelativeTo(String path, String base) {
-    PathFragment relative = PathFragment.create(path).relativeTo(base);
-    assertThat(PathFragment.create(base).getRelative(relative).normalize())
-        .isEqualTo(PathFragment.create(path));
-  }
-
   @Test
   public void testRelativeTo() {
-    assertPath("bar/baz", PathFragment.create("foo/bar/baz").relativeTo("foo"));
-    assertPath("bar/baz", PathFragment.create("/foo/bar/baz").relativeTo("/foo"));
-    assertPath("baz", PathFragment.create("foo/bar/baz").relativeTo("foo/bar"));
-    assertPath("baz", PathFragment.create("/foo/bar/baz").relativeTo("/foo/bar"));
-    assertPath("foo", PathFragment.create("/foo").relativeTo("/"));
-    assertPath("foo", PathFragment.create("foo").relativeTo(""));
-    assertPath("foo/bar", PathFragment.create("foo/bar").relativeTo(""));
-
-    checkRelativeTo("foo/bar/baz", "foo");
-    checkRelativeTo("/foo/bar/baz", "/foo");
-    checkRelativeTo("foo/bar/baz", "foo/bar");
-    checkRelativeTo("/foo/bar/baz", "/foo/bar");
-    checkRelativeTo("/foo", "/");
-    checkRelativeTo("foo", "");
-    checkRelativeTo("foo/bar", "");
+    assertThat(create("foo/bar/baz").relativeTo("foo").getPathString()).isEqualTo("bar/baz");
+    assertThat(create("/foo/bar/baz").relativeTo("/foo").getPathString()).isEqualTo("bar/baz");
+    assertThat(create("foo/bar/baz").relativeTo("foo/bar").getPathString()).isEqualTo("baz");
+    assertThat(create("/foo/bar/baz").relativeTo("/foo/bar").getPathString()).isEqualTo("baz");
+    assertThat(create("/foo").relativeTo("/").getPathString()).isEqualTo("foo");
+    assertThat(create("foo").relativeTo("").getPathString()).isEqualTo("foo");
+    assertThat(create("foo/bar").relativeTo("").getPathString()).isEqualTo("foo/bar");
   }
 
   @Test
   public void testIsAbsolute() {
-    assertThat(PathFragment.create("/absolute/test").isAbsolute()).isTrue();
-    assertThat(PathFragment.create("relative/test").isAbsolute()).isFalse();
-    assertThat(PathFragment.create(new File("/absolute/test")).isAbsolute()).isTrue();
-    assertThat(PathFragment.create(new File("relative/test")).isAbsolute()).isFalse();
+    assertThat(create("/absolute/test").isAbsolute()).isTrue();
+    assertThat(create("relative/test").isAbsolute()).isFalse();
+    assertThat(create(new File("/absolute/test").getPath()).isAbsolute()).isTrue();
+    assertThat(create(new File("relative/test").getPath()).isAbsolute()).isFalse();
   }
 
   @Test
   public void testIsNormalized() {
-    assertThat(PathFragment.create("/absolute/path").isNormalized()).isTrue();
-    assertThat(PathFragment.create("some//path").isNormalized()).isTrue();
-    assertThat(PathFragment.create("some/./path").isNormalized()).isFalse();
-    assertThat(PathFragment.create("../some/path").isNormalized()).isFalse();
-    assertThat(PathFragment.create("some/other/../path").isNormalized()).isFalse();
-    assertThat(PathFragment.create("some/other//tricky..path..").isNormalized()).isTrue();
-    assertThat(PathFragment.create("/some/other//tricky..path..").isNormalized()).isTrue();
+    assertThat(PathFragment.isNormalized("/absolute/path")).isTrue();
+    assertThat(PathFragment.isNormalized("some//path")).isTrue();
+    assertThat(PathFragment.isNormalized("some/./path")).isFalse();
+    assertThat(PathFragment.isNormalized("../some/path")).isFalse();
+    assertThat(PathFragment.isNormalized("./some/path")).isFalse();
+    assertThat(PathFragment.isNormalized("some/path/..")).isFalse();
+    assertThat(PathFragment.isNormalized("some/path/.")).isFalse();
+    assertThat(PathFragment.isNormalized("some/other/../path")).isFalse();
+    assertThat(PathFragment.isNormalized("some/other//tricky..path..")).isTrue();
+    assertThat(PathFragment.isNormalized("/some/other//tricky..path..")).isTrue();
+  }
+
+  @Test
+  public void testContainsUpLevelReferences() {
+    assertThat(PathFragment.containsUplevelReferences("/absolute/path")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("some//path")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("some/./path")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("../some/path")).isTrue();
+    assertThat(PathFragment.containsUplevelReferences("./some/path")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("some/path/..")).isTrue();
+    assertThat(PathFragment.containsUplevelReferences("some/path/.")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("some/other/../path")).isTrue();
+    assertThat(PathFragment.containsUplevelReferences("some/other//tricky..path..")).isFalse();
+    assertThat(PathFragment.containsUplevelReferences("/some/other//tricky..path..")).isFalse();
+
+    // Normalization cannot remove leading uplevel references, so this will be true
+    assertThat(create("../some/path").containsUplevelReferences()).isTrue();
+    // Normalization will remove these, so no uplevel references left
+    assertThat(create("some/path/..").containsUplevelReferences()).isFalse();
   }
 
   @Test
   public void testRootNodeReturnsRootString() {
-    PathFragment rootFragment = PathFragment.create("/");
+    PathFragment rootFragment = create("/");
     assertThat(rootFragment.getPathString()).isEqualTo("/");
   }
 
   @Test
-  public void testGetPathFragmentDoesNotNormalize() {
-    String nonCanonicalPath = "/a/weird/noncanonical/../path/.";
-    assertThat(PathFragment.create(nonCanonicalPath).getPathString()).isEqualTo(nonCanonicalPath);
-  }
-
-  @Test
   public void testGetRelative() {
-    assertThat(PathFragment.create("a").getRelative("b").getPathString()).isEqualTo("a/b");
-    assertThat(PathFragment.create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d");
-    assertThat(PathFragment.create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
-    assertThat(PathFragment.create("a").getRelative("").getPathString()).isEqualTo("a");
-    assertThat(PathFragment.create("/").getRelative("").getPathString()).isEqualTo("/");
+    assertThat(create("a").getRelative("b").getPathString()).isEqualTo("a/b");
+    assertThat(create("a/b").getRelative("c/d").getPathString()).isEqualTo("a/b/c/d");
+    assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
+    assertThat(create("a").getRelative("").getPathString()).isEqualTo("a");
+    assertThat(create("/").getRelative("").getPathString()).isEqualTo("/");
+    assertThat(create("a/b").getRelative("../foo").getPathString()).isEqualTo("a/foo");
+    assertThat(create("/a/b").getRelative("../foo").getPathString()).isEqualTo("/a/foo");
+
+    // Make sure any fast path of PathFragment#getRelative(PathFragment) works
+    assertThat(create("a/b").getRelative(create("../foo")).getPathString()).isEqualTo("a/foo");
+    assertThat(create("/a/b").getRelative(create("../foo")).getPathString()).isEqualTo("/a/foo");
+
+    // Make sure any fast path of PathFragment#getRelative(PathFragment) works
+    assertThat(create("c/d").getRelative(create("/a/b")).getPathString()).isEqualTo("/a/b");
+
+    // Test normalization
+    assertThat(create("a").getRelative(".").getPathString()).isEqualTo("a");
   }
 
   @Test
   public void testGetChildWorks() {
-    PathFragment pf = PathFragment.create("../some/path");
-    assertThat(pf.getChild("hi")).isEqualTo(PathFragment.create("../some/path/hi"));
+    PathFragment pf = create("../some/path");
+    assertThat(pf.getChild("hi")).isEqualTo(create("../some/path/hi"));
   }
 
   @Test
   public void testGetChildRejectsInvalidBaseNames() {
-    PathFragment pf = PathFragment.create("../some/path");
-    assertGetChildFails(pf, ".");
-    assertGetChildFails(pf, "..");
-    assertGetChildFails(pf, "x/y");
-    assertGetChildFails(pf, "/y");
-    assertGetChildFails(pf, "y/");
-    assertGetChildFails(pf, "");
-  }
-
-  private void assertGetChildFails(PathFragment pf, String baseName) {
-    try {
-      pf.getChild(baseName);
-      fail();
-    } catch (Exception e) { /* Expected. */ }
-  }
-
-  // Tests after here test the canonicalization
-  private void assertRegular(String expected, String actual) {
-    // compare string forms
-    assertThat(PathFragment.create(actual).getPathString()).isEqualTo(expected);
-    // compare fragment forms
-    assertThat(PathFragment.create(actual)).isEqualTo(PathFragment.create(expected));
+    PathFragment pf = create("../some/path");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("."));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild(".."));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("x/y"));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("/y"));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild("y/"));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> pf.getChild(""));
   }
 
   @Test
   public void testEmptyPathToEmptyPath() {
-    assertRegular("/", "/");
-    assertRegular("", "");
+    assertThat(create("/").getPathString()).isEqualTo("/");
+    assertThat(create("").getPathString()).isEqualTo("");
   }
 
   @Test
   public void testRedundantSlashes() {
-    assertRegular("/", "///");
-    assertRegular("/foo/bar", "/foo///bar");
-    assertRegular("/foo/bar", "////foo//bar");
+    assertThat(create("///").getPathString()).isEqualTo("/");
+    assertThat(create("/foo///bar").getPathString()).isEqualTo("/foo/bar");
+    assertThat(create("////foo//bar").getPathString()).isEqualTo("/foo/bar");
   }
 
   @Test
   public void testSimpleNameToSimpleName() {
-    assertRegular("/foo", "/foo");
-    assertRegular("foo", "foo");
+    assertThat(create("/foo").getPathString()).isEqualTo("/foo");
+    assertThat(create("foo").getPathString()).isEqualTo("foo");
   }
 
   @Test
   public void testSimplePathToSimplePath() {
-    assertRegular("/foo/bar", "/foo/bar");
-    assertRegular("foo/bar", "foo/bar");
+    assertThat(create("/foo/bar").getPathString()).isEqualTo("/foo/bar");
+    assertThat(create("foo/bar").getPathString()).isEqualTo("foo/bar");
   }
 
   @Test
   public void testStripsTrailingSlash() {
-    assertRegular("/foo/bar", "/foo/bar/");
+    assertThat(create("/foo/bar/").getPathString()).isEqualTo("/foo/bar");
   }
 
   @Test
   public void testGetParentDirectory() {
-    PathFragment fooBarWiz = PathFragment.create("foo/bar/wiz");
-    PathFragment fooBar = PathFragment.create("foo/bar");
-    PathFragment foo = PathFragment.create("foo");
-    PathFragment empty = PathFragment.create("");
+    PathFragment fooBarWiz = create("foo/bar/wiz");
+    PathFragment fooBar = create("foo/bar");
+    PathFragment foo = create("foo");
+    PathFragment empty = create("");
     assertThat(fooBarWiz.getParentDirectory()).isEqualTo(fooBar);
     assertThat(fooBar.getParentDirectory()).isEqualTo(foo);
     assertThat(foo.getParentDirectory()).isEqualTo(empty);
     assertThat(empty.getParentDirectory()).isNull();
 
-    PathFragment fooBarWizAbs = PathFragment.create("/foo/bar/wiz");
-    PathFragment fooBarAbs = PathFragment.create("/foo/bar");
-    PathFragment fooAbs = PathFragment.create("/foo");
-    PathFragment rootAbs = PathFragment.create("/");
+    PathFragment fooBarWizAbs = create("/foo/bar/wiz");
+    PathFragment fooBarAbs = create("/foo/bar");
+    PathFragment fooAbs = create("/foo");
+    PathFragment rootAbs = create("/");
     assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs);
     assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs);
     assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs);
     assertThat(rootAbs.getParentDirectory()).isNull();
-
-    // Note, this is surprising but correct behavior:
-    assertThat(PathFragment.create("/foo/bar/..").getParentDirectory()).isEqualTo(fooBarAbs);
   }
 
   @Test
   public void testSegmentsCount() {
-    assertThat(PathFragment.create("foo/bar").segmentCount()).isEqualTo(2);
-    assertThat(PathFragment.create("/foo/bar").segmentCount()).isEqualTo(2);
-    assertThat(PathFragment.create("foo//bar").segmentCount()).isEqualTo(2);
-    assertThat(PathFragment.create("/foo//bar").segmentCount()).isEqualTo(2);
-    assertThat(PathFragment.create("foo/").segmentCount()).isEqualTo(1);
-    assertThat(PathFragment.create("/foo/").segmentCount()).isEqualTo(1);
-    assertThat(PathFragment.create("foo").segmentCount()).isEqualTo(1);
-    assertThat(PathFragment.create("/foo").segmentCount()).isEqualTo(1);
-    assertThat(PathFragment.create("/").segmentCount()).isEqualTo(0);
-    assertThat(PathFragment.create("").segmentCount()).isEqualTo(0);
+    assertThat(create("foo/bar").segmentCount()).isEqualTo(2);
+    assertThat(create("/foo/bar").segmentCount()).isEqualTo(2);
+    assertThat(create("foo//bar").segmentCount()).isEqualTo(2);
+    assertThat(create("/foo//bar").segmentCount()).isEqualTo(2);
+    assertThat(create("foo/").segmentCount()).isEqualTo(1);
+    assertThat(create("/foo/").segmentCount()).isEqualTo(1);
+    assertThat(create("foo").segmentCount()).isEqualTo(1);
+    assertThat(create("/foo").segmentCount()).isEqualTo(1);
+    assertThat(create("/").segmentCount()).isEqualTo(0);
+    assertThat(create("").segmentCount()).isEqualTo(0);
   }
 
 
   @Test
   public void testGetSegment() {
-    assertThat(PathFragment.create("foo/bar").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("foo/bar").getSegment(1)).isEqualTo("bar");
-    assertThat(PathFragment.create("/foo/bar").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("/foo/bar").getSegment(1)).isEqualTo("bar");
-    assertThat(PathFragment.create("foo/").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("/foo/").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("foo").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("/foo").getSegment(0)).isEqualTo("foo");
+    assertThat(create("foo/bar").getSegment(0)).isEqualTo("foo");
+    assertThat(create("foo/bar").getSegment(1)).isEqualTo("bar");
+    assertThat(create("/foo/bar").getSegment(0)).isEqualTo("foo");
+    assertThat(create("/foo/bar").getSegment(1)).isEqualTo("bar");
+    assertThat(create("foo/").getSegment(0)).isEqualTo("foo");
+    assertThat(create("/foo/").getSegment(0)).isEqualTo("foo");
+    assertThat(create("foo").getSegment(0)).isEqualTo("foo");
+    assertThat(create("/foo").getSegment(0)).isEqualTo("foo");
   }
 
   @Test
   public void testBasename() throws Exception {
-    assertThat(PathFragment.create("foo/bar").getBaseName()).isEqualTo("bar");
-    assertThat(PathFragment.create("/foo/bar").getBaseName()).isEqualTo("bar");
-    assertThat(PathFragment.create("foo/").getBaseName()).isEqualTo("foo");
-    assertThat(PathFragment.create("/foo/").getBaseName()).isEqualTo("foo");
-    assertThat(PathFragment.create("foo").getBaseName()).isEqualTo("foo");
-    assertThat(PathFragment.create("/foo").getBaseName()).isEqualTo("foo");
-    assertThat(PathFragment.create("/").getBaseName()).isEmpty();
-    assertThat(PathFragment.create("").getBaseName()).isEmpty();
+    assertThat(create("foo/bar").getBaseName()).isEqualTo("bar");
+    assertThat(create("/foo/bar").getBaseName()).isEqualTo("bar");
+    assertThat(create("foo/").getBaseName()).isEqualTo("foo");
+    assertThat(create("/foo/").getBaseName()).isEqualTo("foo");
+    assertThat(create("foo").getBaseName()).isEqualTo("foo");
+    assertThat(create("/foo").getBaseName()).isEqualTo("foo");
+    assertThat(create("/").getBaseName()).isEmpty();
+    assertThat(create("").getBaseName()).isEmpty();
   }
 
   @Test
   public void testFileExtension() throws Exception {
-    assertThat(PathFragment.create("foo.bar").getFileExtension()).isEqualTo("bar");
-    assertThat(PathFragment.create("foo.barr").getFileExtension()).isEqualTo("barr");
-    assertThat(PathFragment.create("foo.b").getFileExtension()).isEqualTo("b");
-    assertThat(PathFragment.create("foo.").getFileExtension()).isEmpty();
-    assertThat(PathFragment.create("foo").getFileExtension()).isEmpty();
-    assertThat(PathFragment.create(".").getFileExtension()).isEmpty();
-    assertThat(PathFragment.create("").getFileExtension()).isEmpty();
-    assertThat(PathFragment.create("foo/bar.baz").getFileExtension()).isEqualTo("baz");
-    assertThat(PathFragment.create("foo.bar.baz").getFileExtension()).isEqualTo("baz");
-    assertThat(PathFragment.create("foo.bar/baz").getFileExtension()).isEmpty();
-  }
-
-  private static void assertPath(String expected, PathFragment actual) {
-    assertThat(actual.getPathString()).isEqualTo(expected);
+    assertThat(create("foo.bar").getFileExtension()).isEqualTo("bar");
+    assertThat(create("foo.barr").getFileExtension()).isEqualTo("barr");
+    assertThat(create("foo.b").getFileExtension()).isEqualTo("b");
+    assertThat(create("foo.").getFileExtension()).isEmpty();
+    assertThat(create("foo").getFileExtension()).isEmpty();
+    assertThat(create(".").getFileExtension()).isEmpty();
+    assertThat(create("").getFileExtension()).isEmpty();
+    assertThat(create("foo/bar.baz").getFileExtension()).isEqualTo("baz");
+    assertThat(create("foo.bar.baz").getFileExtension()).isEqualTo("baz");
+    assertThat(create("foo.bar/baz").getFileExtension()).isEmpty();
   }
 
   @Test
   public void testReplaceName() throws Exception {
-    assertPath("foo/baz", PathFragment.create("foo/bar").replaceName("baz"));
-    assertPath("/foo/baz", PathFragment.create("/foo/bar").replaceName("baz"));
-    assertPath("foo", PathFragment.create("foo/bar").replaceName(""));
-    assertPath("baz", PathFragment.create("foo/").replaceName("baz"));
-    assertPath("/baz", PathFragment.create("/foo/").replaceName("baz"));
-    assertPath("baz", PathFragment.create("foo").replaceName("baz"));
-    assertPath("/baz", PathFragment.create("/foo").replaceName("baz"));
-    assertThat(PathFragment.create("/").replaceName("baz")).isNull();
-    assertThat(PathFragment.create("/").replaceName("")).isNull();
-    assertThat(PathFragment.create("").replaceName("baz")).isNull();
-    assertThat(PathFragment.create("").replaceName("")).isNull();
+    assertThat(create("foo/bar").replaceName("baz").getPathString()).isEqualTo("foo/baz");
+    assertThat(create("/foo/bar").replaceName("baz").getPathString()).isEqualTo("/foo/baz");
+    assertThat(create("foo/bar").replaceName("").getPathString()).isEqualTo("foo");
+    assertThat(create("foo/").replaceName("baz").getPathString()).isEqualTo("baz");
+    assertThat(create("/foo/").replaceName("baz").getPathString()).isEqualTo("/baz");
+    assertThat(create("foo").replaceName("baz").getPathString()).isEqualTo("baz");
+    assertThat(create("/foo").replaceName("baz").getPathString()).isEqualTo("/baz");
+    assertThat(create("/").replaceName("baz")).isNull();
+    assertThat(create("/").replaceName("")).isNull();
+    assertThat(create("").replaceName("baz")).isNull();
+    assertThat(create("").replaceName("")).isNull();
 
-    assertPath("foo/bar/baz", PathFragment.create("foo/bar").replaceName("bar/baz"));
-    assertPath("foo/bar/baz", PathFragment.create("foo/bar").replaceName("bar/baz/"));
+    assertThat(create("foo/bar").replaceName("bar/baz").getPathString()).isEqualTo("foo/bar/baz");
+    assertThat(create("foo/bar").replaceName("bar/baz/").getPathString()).isEqualTo("foo/bar/baz");
 
     // Absolute path arguments will clobber the original path.
-    assertPath("/absolute", PathFragment.create("foo/bar").replaceName("/absolute"));
-    assertPath("/", PathFragment.create("foo/bar").replaceName("/"));
+    assertThat(create("foo/bar").replaceName("/absolute").getPathString()).isEqualTo("/absolute");
+    assertThat(create("foo/bar").replaceName("/").getPathString()).isEqualTo("/");
   }
   @Test
   public void testSubFragment() throws Exception {
-    assertPath("/foo/bar/baz",
-        PathFragment.create("/foo/bar/baz").subFragment(0, 3));
-    assertPath("foo/bar/baz",
-        PathFragment.create("foo/bar/baz").subFragment(0, 3));
-    assertPath("/foo/bar",
-               PathFragment.create("/foo/bar/baz").subFragment(0, 2));
-    assertPath("bar/baz",
-               PathFragment.create("/foo/bar/baz").subFragment(1, 3));
-    assertPath("/foo",
-               PathFragment.create("/foo/bar/baz").subFragment(0, 1));
-    assertPath("bar",
-               PathFragment.create("/foo/bar/baz").subFragment(1, 2));
-    assertPath("baz", PathFragment.create("/foo/bar/baz").subFragment(2, 3));
-    assertPath("/", PathFragment.create("/foo/bar/baz").subFragment(0, 0));
-    assertPath("", PathFragment.create("foo/bar/baz").subFragment(0, 0));
-    assertPath("", PathFragment.create("foo/bar/baz").subFragment(1, 1));
-    assertPath("/foo/bar/baz", PathFragment.create("/foo/bar/baz").subFragment(0));
-    assertPath("bar/baz", PathFragment.create("/foo/bar/baz").subFragment(1));
-    try {
-      fail("unexpectedly succeeded: " + PathFragment.create("foo/bar/baz").subFragment(3, 2));
-    } catch (IndexOutOfBoundsException e) { /* Expected. */ }
-    try {
-      fail("unexpectedly succeeded: " + PathFragment.create("foo/bar/baz").subFragment(4, 4));
-    } catch (IndexOutOfBoundsException e) { /* Expected. */ }
+    assertThat(create("/foo/bar/baz").subFragment(0, 3).getPathString()).isEqualTo("/foo/bar/baz");
+    assertThat(create("foo/bar/baz").subFragment(0, 3).getPathString()).isEqualTo("foo/bar/baz");
+    assertThat(create("/foo/bar/baz").subFragment(0, 2).getPathString()).isEqualTo("/foo/bar");
+    assertThat(create("/foo/bar/baz").subFragment(1, 3).getPathString()).isEqualTo("bar/baz");
+    assertThat(create("/foo/bar/baz").subFragment(0, 1).getPathString()).isEqualTo("/foo");
+    assertThat(create("/foo/bar/baz").subFragment(1, 2).getPathString()).isEqualTo("bar");
+    assertThat(create("/foo/bar/baz").subFragment(2, 3).getPathString()).isEqualTo("baz");
+    assertThat(create("/foo/bar/baz").subFragment(0, 0).getPathString()).isEqualTo("/");
+    assertThat(create("foo/bar/baz").subFragment(0, 0).getPathString()).isEqualTo("");
+    assertThat(create("foo/bar/baz").subFragment(1, 1).getPathString()).isEqualTo("");
+
+    assertThat(create("/foo/bar/baz").subFragment(0).getPathString()).isEqualTo("/foo/bar/baz");
+    assertThat(create("foo/bar/baz").subFragment(0).getPathString()).isEqualTo("foo/bar/baz");
+    assertThat(create("/foo/bar/baz").subFragment(1).getPathString()).isEqualTo("bar/baz");
+    assertThat(create("foo/bar/baz").subFragment(1).getPathString()).isEqualTo("bar/baz");
+    assertThat(create("foo/bar/baz").subFragment(2).getPathString()).isEqualTo("baz");
+    assertThat(create("foo/bar/baz").subFragment(3).getPathString()).isEqualTo("");
+
+    MoreAsserts.expectThrows(
+        IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(3, 2));
+    MoreAsserts.expectThrows(
+        IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(4, 4));
+    MoreAsserts.expectThrows(
+        IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(3, 2));
+    MoreAsserts.expectThrows(
+        IndexOutOfBoundsException.class, () -> create("foo/bar/baz").subFragment(4));
   }
 
   @Test
   public void testStartsWith() {
-    PathFragment foobar = PathFragment.create("/foo/bar");
-    PathFragment foobarRelative = PathFragment.create("foo/bar");
+    PathFragment foobar = create("/foo/bar");
+    PathFragment foobarRelative = create("foo/bar");
 
     // (path, prefix) => true
     assertThat(foobar.startsWith(foobar)).isTrue();
-    assertThat(foobar.startsWith(PathFragment.create("/"))).isTrue();
-    assertThat(foobar.startsWith(PathFragment.create("/foo"))).isTrue();
-    assertThat(foobar.startsWith(PathFragment.create("/foo/"))).isTrue();
-    assertThat(foobar.startsWith(PathFragment.create("/foo/bar/")))
-        .isTrue(); // Includes trailing slash.
+    assertThat(foobar.startsWith(create("/"))).isTrue();
+    assertThat(foobar.startsWith(create("/foo"))).isTrue();
+    assertThat(foobar.startsWith(create("/foo/"))).isTrue();
+    assertThat(foobar.startsWith(create("/foo/bar/"))).isTrue(); // Includes trailing slash.
 
     // (prefix, path) => false
-    assertThat(PathFragment.create("/foo").startsWith(foobar)).isFalse();
-    assertThat(PathFragment.create("/").startsWith(foobar)).isFalse();
+    assertThat(create("/foo").startsWith(foobar)).isFalse();
+    assertThat(create("/").startsWith(foobar)).isFalse();
 
     // (absolute, relative) => false
     assertThat(foobar.startsWith(foobarRelative)).isFalse();
@@ -373,69 +339,53 @@
 
     // (relative path, relative prefix) => true
     assertThat(foobarRelative.startsWith(foobarRelative)).isTrue();
-    assertThat(foobarRelative.startsWith(PathFragment.create("foo"))).isTrue();
-    assertThat(foobarRelative.startsWith(PathFragment.create(""))).isTrue();
+    assertThat(foobarRelative.startsWith(create("foo"))).isTrue();
+    assertThat(foobarRelative.startsWith(create(""))).isTrue();
 
     // (path, sibling) => false
-    assertThat(PathFragment.create("/foo/wiz").startsWith(foobar)).isFalse();
-    assertThat(foobar.startsWith(PathFragment.create("/foo/wiz"))).isFalse();
-
-    // Does not normalize.
-    PathFragment foodotbar = PathFragment.create("foo/./bar");
-    assertThat(foodotbar.startsWith(foodotbar)).isTrue();
-    assertThat(foodotbar.startsWith(PathFragment.create("foo/."))).isTrue();
-    assertThat(foodotbar.startsWith(PathFragment.create("foo/./"))).isTrue();
-    assertThat(foodotbar.startsWith(PathFragment.create("foo/./bar"))).isTrue();
-    assertThat(foodotbar.startsWith(PathFragment.create("foo/bar"))).isFalse();
+    assertThat(create("/foo/wiz").startsWith(foobar)).isFalse();
+    assertThat(foobar.startsWith(create("/foo/wiz"))).isFalse();
   }
 
   @Test
   public void testCheckAllPathsStartWithButAreNotEqualTo() {
     // Check passes:
-    PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a/c"),
-        PathFragment.create("a"));
+    PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a/c"), create("a"));
 
     // Check trivially passes:
-    PathFragment.checkAllPathsAreUnder(ImmutableList.<PathFragment>of(),
-        PathFragment.create("a"));
+    PathFragment.checkAllPathsAreUnder(ImmutableList.<PathFragment>of(), create("a"));
 
     // Check fails when some path does not start with startingWithPath:
-    try {
-      PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "b/c"),
-          PathFragment.create("a"));
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
+    MoreAsserts.expectThrows(
+        IllegalArgumentException.class,
+        () -> PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "b/c"), create("a")));
 
     // Check fails when some path is equal to startingWithPath:
-    try {
-      PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a"),
-          PathFragment.create("a"));
-      fail();
-    } catch (IllegalArgumentException expected) {
-    }
+    MoreAsserts.expectThrows(
+        IllegalArgumentException.class,
+        () -> PathFragment.checkAllPathsAreUnder(toPathsSet("a/b", "a"), create("a")));
   }
 
   @Test
   public void testEndsWith() {
-    PathFragment foobar = PathFragment.create("/foo/bar");
-    PathFragment foobarRelative = PathFragment.create("foo/bar");
+    PathFragment foobar = create("/foo/bar");
+    PathFragment foobarRelative = create("foo/bar");
 
     // (path, suffix) => true
     assertThat(foobar.endsWith(foobar)).isTrue();
-    assertThat(foobar.endsWith(PathFragment.create("bar"))).isTrue();
-    assertThat(foobar.endsWith(PathFragment.create("foo/bar"))).isTrue();
-    assertThat(foobar.endsWith(PathFragment.create("/foo/bar"))).isTrue();
-    assertThat(foobar.endsWith(PathFragment.create("/bar"))).isFalse();
+    assertThat(foobar.endsWith(create("bar"))).isTrue();
+    assertThat(foobar.endsWith(create("foo/bar"))).isTrue();
+    assertThat(foobar.endsWith(create("/foo/bar"))).isTrue();
+    assertThat(foobar.endsWith(create("/bar"))).isFalse();
 
     // (prefix, path) => false
-    assertThat(PathFragment.create("/foo").endsWith(foobar)).isFalse();
-    assertThat(PathFragment.create("/").endsWith(foobar)).isFalse();
+    assertThat(create("/foo").endsWith(foobar)).isFalse();
+    assertThat(create("/").endsWith(foobar)).isFalse();
 
     // (suffix, path) => false
-    assertThat(PathFragment.create("/bar").endsWith(foobar)).isFalse();
-    assertThat(PathFragment.create("bar").endsWith(foobar)).isFalse();
-    assertThat(PathFragment.create("").endsWith(foobar)).isFalse();
+    assertThat(create("/bar").endsWith(foobar)).isFalse();
+    assertThat(create("bar").endsWith(foobar)).isFalse();
+    assertThat(create("").endsWith(foobar)).isFalse();
 
     // (absolute, relative) => true
     assertThat(foobar.endsWith(foobarRelative)).isTrue();
@@ -445,18 +395,32 @@
 
     // (relative path, relative prefix) => true
     assertThat(foobarRelative.endsWith(foobarRelative)).isTrue();
-    assertThat(foobarRelative.endsWith(PathFragment.create("bar"))).isTrue();
-    assertThat(foobarRelative.endsWith(PathFragment.create(""))).isTrue();
+    assertThat(foobarRelative.endsWith(create("bar"))).isTrue();
+    assertThat(foobarRelative.endsWith(create(""))).isTrue();
 
     // (path, sibling) => false
-    assertThat(PathFragment.create("/foo/wiz").endsWith(foobar)).isFalse();
-    assertThat(foobar.endsWith(PathFragment.create("/foo/wiz"))).isFalse();
+    assertThat(create("/foo/wiz").endsWith(foobar)).isFalse();
+    assertThat(foobar.endsWith(create("/foo/wiz"))).isFalse();
+  }
+
+  @Test
+  public void testToRelative() {
+    assertThat(create("/foo/bar").toRelative()).isEqualTo(create("foo/bar"));
+    assertThat(create("/").toRelative()).isEqualTo(create(""));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").toRelative());
+  }
+
+  @Test
+  public void testGetDriveStr() {
+    assertThat(create("/foo/bar").getDriveStr()).isEqualTo("/");
+    assertThat(create("/").getDriveStr()).isEqualTo("/");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").getDriveStr());
   }
 
   static List<PathFragment> toPaths(List<String> strs) {
     List<PathFragment> paths = Lists.newArrayList();
     for (String s : strs) {
-      paths.add(PathFragment.create(s));
+      paths.add(create(s));
     }
     return paths;
   }
@@ -464,15 +428,26 @@
   static ImmutableSet<PathFragment> toPathsSet(String... strs) {
     ImmutableSet.Builder<PathFragment> builder = ImmutableSet.builder();
     for (String str : strs) {
-      builder.add(PathFragment.create(str));
+      builder.add(create(str));
     }
     return builder.build();
   }
 
   @Test
   public void testCompareTo() throws Exception {
-    List<String> pathStrs = ImmutableList.of(
-        "", "/", "//", ".", "/./", "foo/.//bar", "foo", "/foo", "foo/bar", "foo/Bar", "Foo/bar");
+    List<String> pathStrs =
+        ImmutableList.of(
+            "",
+            "/",
+            "foo",
+            "/foo",
+            "foo/bar",
+            "foo.bar",
+            "foo/bar.baz",
+            "foo/bar/baz",
+            "foo/barfile",
+            "foo/Bar",
+            "Foo/bar");
     List<PathFragment> paths = toPaths(pathStrs);
     // First test that compareTo is self-consistent.
     for (PathFragment x : paths) {
@@ -493,36 +468,80 @@
         }
       }
     }
-    // Now test that compareTo does what we expect.  The exact ordering here doesn't matter much,
-    // but there are three things to notice: 1. absolute < relative, 2. comparison is lexicographic
-    // 3. repeated slashes are ignored. (PathFragment("//") prints as "/").
+    // Now test that compareTo does what we expect.  The exact ordering here doesn't matter much.
     Collections.shuffle(paths);
     Collections.sort(paths);
-    List<PathFragment> expectedOrder = toPaths(ImmutableList.of(
-        "/", "//", "/./", "/foo", "", ".", "Foo/bar", "foo", "foo/.//bar", "foo/Bar", "foo/bar"));
+    List<PathFragment> expectedOrder =
+        toPaths(
+            ImmutableList.of(
+                "",
+                "/",
+                "/foo",
+                "Foo/bar",
+                "foo",
+                "foo.bar",
+                "foo/Bar",
+                "foo/bar",
+                "foo/bar.baz",
+                "foo/bar/baz",
+                "foo/barfile"));
     assertThat(paths).isEqualTo(expectedOrder);
   }
 
   @Test
   public void testGetSafePathString() {
-    assertThat(PathFragment.create("/").getSafePathString()).isEqualTo("/");
-    assertThat(PathFragment.create("/abc").getSafePathString()).isEqualTo("/abc");
-    assertThat(PathFragment.create("").getSafePathString()).isEqualTo(".");
+    assertThat(create("/").getSafePathString()).isEqualTo("/");
+    assertThat(create("/abc").getSafePathString()).isEqualTo("/abc");
+    assertThat(create("").getSafePathString()).isEqualTo(".");
     assertThat(PathFragment.EMPTY_FRAGMENT.getSafePathString()).isEqualTo(".");
-    assertThat(PathFragment.create("abc/def").getSafePathString()).isEqualTo("abc/def");
+    assertThat(create("abc/def").getSafePathString()).isEqualTo("abc/def");
   }
 
   @Test
   public void testNormalize() {
-    assertThat(PathFragment.create("/a/b").normalize()).isEqualTo(PathFragment.create("/a/b"));
-    assertThat(PathFragment.create("/a/./b").normalize()).isEqualTo(PathFragment.create("/a/b"));
-    assertThat(PathFragment.create("/a/../b").normalize()).isEqualTo(PathFragment.create("/b"));
-    assertThat(PathFragment.create("a/b").normalize()).isEqualTo(PathFragment.create("a/b"));
-    assertThat(PathFragment.create("a/../../b").normalize()).isEqualTo(PathFragment.create("../b"));
-    assertThat(PathFragment.create("a/../..").normalize()).isEqualTo(PathFragment.create(".."));
-    assertThat(PathFragment.create("a/../b").normalize()).isEqualTo(PathFragment.create("b"));
-    assertThat(PathFragment.create("a/b/../b").normalize()).isEqualTo(PathFragment.create("a/b"));
-    assertThat(PathFragment.create("/..").normalize()).isEqualTo(PathFragment.create("/.."));
+    assertThat(create("/a/b")).isEqualTo(create("/a/b"));
+    assertThat(create("/a/./b")).isEqualTo(create("/a/b"));
+    assertThat(create("/a/../b")).isEqualTo(create("/b"));
+    assertThat(create("a/b")).isEqualTo(create("a/b"));
+    assertThat(create("a/../../b")).isEqualTo(create("../b"));
+    assertThat(create("a/../..")).isEqualTo(create(".."));
+    assertThat(create("a/../b")).isEqualTo(create("b"));
+    assertThat(create("a/b/../b")).isEqualTo(create("a/b"));
+    assertThat(create("/..")).isEqualTo(create("/.."));
+    assertThat(create("..")).isEqualTo(create(".."));
+  }
+
+  @Test
+  public void testSegments() {
+    assertThat(create("").segmentCount()).isEqualTo(0);
+    assertThat(create("a").segmentCount()).isEqualTo(1);
+    assertThat(create("a/b").segmentCount()).isEqualTo(2);
+    assertThat(create("a/b/c").segmentCount()).isEqualTo(3);
+    assertThat(create("/").segmentCount()).isEqualTo(0);
+    assertThat(create("/a").segmentCount()).isEqualTo(1);
+    assertThat(create("/a/b").segmentCount()).isEqualTo(2);
+    assertThat(create("/a/b/c").segmentCount()).isEqualTo(3);
+
+    assertThat(create("").getSegments()).isEmpty();
+    assertThat(create("a").getSegments()).containsExactly("a").inOrder();
+    assertThat(create("a/b").getSegments()).containsExactly("a", "b").inOrder();
+    assertThat(create("a/b/c").getSegments()).containsExactly("a", "b", "c").inOrder();
+    assertThat(create("/").getSegments()).isEmpty();
+    assertThat(create("/a").getSegments()).containsExactly("a").inOrder();
+    assertThat(create("/a/b").getSegments()).containsExactly("a", "b").inOrder();
+    assertThat(create("/a/b/c").getSegments()).containsExactly("a", "b", "c").inOrder();
+
+    assertThat(create("a").getSegment(0)).isEqualTo("a");
+    assertThat(create("a/b").getSegment(0)).isEqualTo("a");
+    assertThat(create("a/b").getSegment(1)).isEqualTo("b");
+    assertThat(create("a/b/c").getSegment(2)).isEqualTo("c");
+    assertThat(create("/a").getSegment(0)).isEqualTo("a");
+    assertThat(create("/a/b").getSegment(0)).isEqualTo("a");
+    assertThat(create("/a/b").getSegment(1)).isEqualTo("b");
+    assertThat(create("/a/b/c").getSegment(2)).isEqualTo("c");
+
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("").getSegment(0));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("a/b").getSegment(2));
   }
 
   @Test
@@ -552,7 +571,7 @@
   }
 
   private void checkSerialization(String pathFragmentString, int expectedSize) throws Exception {
-    PathFragment a = PathFragment.create(pathFragmentString);
+    PathFragment a = create(pathFragmentString);
     byte[] sa = TestUtils.serializeObject(a);
     assertThat(sa).hasLength(expectedSize);
 
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
index 1f18d07..b2a8a52 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/PathFragmentWindowsTest.java
@@ -14,9 +14,9 @@
 package com.google.devtools.build.lib.vfs;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-import static org.junit.Assert.fail;
+import static com.google.devtools.build.lib.vfs.PathFragment.create;
 
+import com.google.devtools.build.lib.testutil.MoreAsserts;
 import java.io.File;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -30,303 +30,199 @@
 
   @Test
   public void testWindowsSeparator() {
-    assertThat(PathFragment.create("bar\\baz").toString()).isEqualTo("bar/baz");
-    assertThat(PathFragment.create("c:\\bar\\baz").toString()).isEqualTo("C:/bar/baz");
+    assertThat(create("bar\\baz").toString()).isEqualTo("bar/baz");
+    assertThat(create("c:\\bar\\baz").toString()).isEqualTo("C:/bar/baz");
   }
 
   @Test
   public void testIsAbsoluteWindows() {
-    assertThat(PathFragment.create("C:/").isAbsolute()).isTrue();
-    assertThat(PathFragment.create("C:/").isAbsolute()).isTrue();
-    assertThat(PathFragment.create("C:/foo").isAbsolute()).isTrue();
-    assertThat(PathFragment.create("d:/foo/bar").isAbsolute()).isTrue();
+    assertThat(create("C:/").isAbsolute()).isTrue();
+    assertThat(create("C:/").isAbsolute()).isTrue();
+    assertThat(create("C:/foo").isAbsolute()).isTrue();
+    assertThat(create("d:/foo/bar").isAbsolute()).isTrue();
 
-    assertThat(PathFragment.create("*:/").isAbsolute()).isFalse();
+    assertThat(create("*:/").isAbsolute()).isFalse();
   }
 
   @Test
   public void testAbsoluteAndAbsoluteLookingPaths() {
-    PathFragment p1 = PathFragment.create("/c");
-    assertThat(p1.isAbsolute()).isTrue();
-    assertThat(p1.getDriveLetter()).isEqualTo('\0');
-    assertThat(p1.getSegments()).containsExactly("c");
+    assertThat(create("/c").isAbsolute()).isTrue();
+    assertThat(create("/c").getSegments()).containsExactly("c");
 
-    PathFragment p2 = PathFragment.create("/c/");
-    assertThat(p2.isAbsolute()).isTrue();
-    assertThat(p2.getDriveLetter()).isEqualTo('\0');
-    assertThat(p2.getSegments()).containsExactly("c");
+    assertThat(create("/c/").isAbsolute()).isTrue();
+    assertThat(create("/c/").getSegments()).containsExactly("c");
 
-    PathFragment p3 = PathFragment.create("C:/");
-    assertThat(p3.isAbsolute()).isTrue();
-    assertThat(p3.getDriveLetter()).isEqualTo('C');
-    assertThat(p3.getSegments()).isEmpty();
+    assertThat(create("C:/").isAbsolute()).isTrue();
+    assertThat(create("C:/").getSegments()).isEmpty();
 
-    PathFragment p5 = PathFragment.create("/c:");
+    PathFragment p5 = create("/c:");
     assertThat(p5.isAbsolute()).isTrue();
-    assertThat(p5.getDriveLetter()).isEqualTo('\0');
     assertThat(p5.getSegments()).containsExactly("c:");
+    assertThat(create("C:").isAbsolute()).isFalse();
 
-    assertThat(p1).isEqualTo(p2);
-    assertThat(p1).isNotEqualTo(p3);
-    assertThat(p1).isNotEqualTo(p5);
-    assertThat(p3).isNotEqualTo(p5);
+    assertThat(create("/c:").isAbsolute()).isTrue();
+    assertThat(create("/c:").getSegments()).containsExactly("c:");
+
+    assertThat(create("/c")).isEqualTo(create("/c/"));
+    assertThat(create("/c")).isNotEqualTo(create("C:/"));
+    assertThat(create("/c")).isNotEqualTo(create("C:"));
+    assertThat(create("/c")).isNotEqualTo(create("/c:"));
+    assertThat(create("C:/")).isNotEqualTo(create("C:"));
+    assertThat(create("C:/")).isNotEqualTo(create("/c:"));
   }
 
   @Test
   public void testIsAbsoluteWindowsBackslash() {
-    assertThat(PathFragment.create(new File("C:\\blah")).isAbsolute()).isTrue();
-    assertThat(PathFragment.create(new File("C:\\")).isAbsolute()).isTrue();
-    assertThat(PathFragment.create(new File("\\blah")).isAbsolute()).isTrue();
-    assertThat(PathFragment.create(new File("\\")).isAbsolute()).isTrue();
-  }
-
-  @Test
-  public void testIsNormalizedWindows() {
-    assertThat(PathFragment.create("C:/").isNormalized()).isTrue();
-    assertThat(PathFragment.create("C:/absolute/path").isNormalized()).isTrue();
-    assertThat(PathFragment.create("C:/absolute/./path").isNormalized()).isFalse();
-    assertThat(PathFragment.create("C:/absolute/../path").isNormalized()).isFalse();
+    assertThat(create(new File("C:\\blah").getPath()).isAbsolute()).isTrue();
+    assertThat(create(new File("C:\\").getPath()).isAbsolute()).isTrue();
+    assertThat(create(new File("\\blah").getPath()).isAbsolute()).isTrue();
+    assertThat(create(new File("\\").getPath()).isAbsolute()).isTrue();
   }
 
   @Test
   public void testRootNodeReturnsRootStringWindows() {
-    PathFragment rootFragment = PathFragment.create("C:/");
-    assertThat(rootFragment.getPathString()).isEqualTo("C:/");
+    assertThat(create("C:/").getPathString()).isEqualTo("C:/");
   }
 
   @Test
   public void testGetRelativeWindows() {
-    assertThat(PathFragment.create("C:/a").getRelative("b").getPathString()).isEqualTo("C:/a/b");
-    assertThat(PathFragment.create("C:/a/b").getRelative("c/d").getPathString())
-        .isEqualTo("C:/a/b/c/d");
-    assertThat(PathFragment.create("C:/a").getRelative("C:/b").getPathString()).isEqualTo("C:/b");
-    assertThat(PathFragment.create("C:/a/b").getRelative("C:/c/d").getPathString())
-        .isEqualTo("C:/c/d");
-    assertThat(PathFragment.create("a").getRelative("C:/b").getPathString()).isEqualTo("C:/b");
-    assertThat(PathFragment.create("a/b").getRelative("C:/c/d").getPathString())
-        .isEqualTo("C:/c/d");
-  }
-
-  private void assertGetRelative(String path, String relative, PathFragment expected)
-      throws Exception {
-    PathFragment actual = PathFragment.create(path).getRelative(relative);
-    assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
-    assertThat(actual).isEqualTo(expected);
-    assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter());
-    assertThat(actual.hashCode()).isEqualTo(expected.hashCode());
-  }
-
-  private void assertRelativeTo(String path, String relativeTo, String... expectedPathSegments)
-      throws Exception {
-    PathFragment expected = PathFragment.createAlreadyInterned('\0', false, expectedPathSegments);
-    PathFragment actual = PathFragment.create(path).relativeTo(relativeTo);
-    assertThat(actual.getPathString()).isEqualTo(expected.getPathString());
-    assertThat(actual).isEqualTo(expected);
-    assertThat(actual.getDriveLetter()).isEqualTo(expected.getDriveLetter());
-    assertThat(actual.hashCode()).isEqualTo(expected.hashCode());
-  }
-
-  private void assertCantComputeRelativeTo(String path, String relativeTo) throws Exception {
-    try {
-      PathFragment.create(path).relativeTo(relativeTo);
-      fail("expected failure");
-    } catch (Exception e) {
-      assertThat(e).hasMessageThat().contains("is not beneath");
-    }
-  }
-
-  private static PathFragment makePath(char drive, boolean absolute, String... segments) {
-    return PathFragment.createAlreadyInterned(drive, absolute, segments);
+    assertThat(create("C:/a").getRelative("b").getPathString()).isEqualTo("C:/a/b");
+    assertThat(create("C:/a/b").getRelative("c/d").getPathString()).isEqualTo("C:/a/b/c/d");
+    assertThat(create("C:/a").getRelative("C:/b").getPathString()).isEqualTo("C:/b");
+    assertThat(create("C:/a/b").getRelative("C:/c/d").getPathString()).isEqualTo("C:/c/d");
+    assertThat(create("a").getRelative("C:/b").getPathString()).isEqualTo("C:/b");
+    assertThat(create("a/b").getRelative("C:/c/d").getPathString()).isEqualTo("C:/c/d");
   }
 
   @Test
   public void testGetRelativeMixed() throws Exception {
-    assertGetRelative("a", "b", makePath('\0', false, "a", "b"));
-    assertGetRelative("a", "/b", makePath('\0', true, "b"));
-    assertGetRelative("a", "E:/b", makePath('E', true, "b"));
+    assertThat(create("a").getRelative("b")).isEqualTo(create("a/b"));
+    assertThat(create("a").getRelative("/b")).isEqualTo(create("/b"));
+    assertThat(create("a").getRelative("E:/b")).isEqualTo(create("E:/b"));
 
-    assertGetRelative("/a", "b", makePath('\0', true, "a", "b"));
-    assertGetRelative("/a", "/b", makePath('\0', true, "b"));
-    assertGetRelative("/a", "E:/b", makePath('E', true, "b"));
+    assertThat(create("/a").getRelative("b")).isEqualTo(create("/a/b"));
+    assertThat(create("/a").getRelative("/b")).isEqualTo(create("/b"));
+    assertThat(create("/a").getRelative("E:/b")).isEqualTo(create("E:/b"));
 
-    assertGetRelative("D:/a", "b", makePath('D', true, "a", "b"));
-    assertGetRelative("D:/a", "/b", makePath('D', true, "b"));
-    assertGetRelative("D:/a", "E:/b", makePath('E', true, "b"));
+    assertThat(create("D:/a").getRelative("b")).isEqualTo(create("D:/a/b"));
+    assertThat(create("D:/a").getRelative("/b")).isEqualTo(create("/b"));
+    assertThat(create("D:/a").getRelative("E:/b")).isEqualTo(create("E:/b"));
   }
 
   @Test
   public void testRelativeTo() throws Exception {
-    assertRelativeTo("", "");
-    assertCantComputeRelativeTo("", "a");
+    assertThat(create("").relativeTo("").getPathString()).isEqualTo("");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("").relativeTo("a"));
 
-    assertRelativeTo("a", "", "a");
-    assertRelativeTo("a", "a");
-    assertCantComputeRelativeTo("a", "b");
-    assertRelativeTo("a/b", "a", "b");
+    assertThat(create("a").relativeTo("")).isEqualTo(create("a"));
+    assertThat(create("a").relativeTo("a").getPathString()).isEqualTo("");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("a").relativeTo("b"));
+    assertThat(create("a/b").relativeTo("a")).isEqualTo(create("b"));
 
-    assertCantComputeRelativeTo("C:/", "");
-    assertRelativeTo("C:/", "C:/");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("C:/").relativeTo(""));
+    assertThat(create("C:/").relativeTo("C:/").getPathString()).isEqualTo("");
   }
 
   @Test
   public void testGetChildWorks() {
-    PathFragment pf = PathFragment.create("../some/path");
-    assertThat(pf.getChild("hi")).isEqualTo(PathFragment.create("../some/path/hi"));
-  }
-
-  // Tests after here test the canonicalization
-  private void assertRegular(String expected, String actual) {
-    PathFragment exp = PathFragment.create(expected);
-    PathFragment act = PathFragment.create(actual);
-    assertThat(exp.getPathString()).isEqualTo(expected);
-    assertThat(act.getPathString()).isEqualTo(expected);
-    assertThat(act).isEqualTo(exp);
-    assertThat(act.hashCode()).isEqualTo(exp.hashCode());
+    assertThat(create("../some/path").getChild("hi")).isEqualTo(create("../some/path/hi"));
   }
 
   @Test
   public void testEmptyPathToEmptyPathWindows() {
-    assertRegular("C:/", "C:/");
-  }
-
-  private void assertAllEqual(PathFragment... ps) {
-    assertThat(ps.length).isGreaterThan(1);
-    for (int i = 1; i < ps.length; i++) {
-      String msg = "comparing items 0 and " + i;
-      assertWithMessage(msg + " for getPathString")
-          .that(ps[i].getPathString())
-          .isEqualTo(ps[0].getPathString());
-      assertWithMessage(msg + " for equals").that(ps[0]).isEqualTo(ps[i]);
-      assertWithMessage(msg + " for hashCode").that(ps[0].hashCode()).isEqualTo(ps[i].hashCode());
-    }
-  }
-
-  @Test
-  public void testEmptyRelativePathToEmptyPathWindows() {
-    // Surprising but correct behavior: a PathFragment made of just a drive identifier (and not the
-    // absolute path "C:/") is equal not only to the empty fragment, but (therefore) also to other
-    // drive identifiers.
-    // This makes sense if you consider that these are still empty paths, the drive letter adds no
-    // information to the path itself.
-    assertAllEqual(
-        PathFragment.EMPTY_FRAGMENT,
-        PathFragment.createAlreadyInterned('\0', false, new String[0]),
-        PathFragment.createAlreadyInterned('C', false, new String[0]),
-        PathFragment.createAlreadyInterned('D', false, new String[0]));
-    assertAllEqual(PathFragment.create("/c"), PathFragment.create("/c/"));
-    assertThat(PathFragment.create("C:/")).isNotEqualTo(PathFragment.create("/c"));
-    assertThat(PathFragment.create("C:/foo")).isNotEqualTo(PathFragment.create("/c/foo"));
-
-    assertThat(PathFragment.create("C:/")).isNotEqualTo(PathFragment.create("C:"));
-    assertThat(PathFragment.create("C:/").getPathString())
-        .isNotEqualTo(PathFragment.create("C:").getPathString());
+    assertThat(create("C:/")).isEqualTo(create("C:/"));
   }
 
   @Test
   public void testWindowsVolumeUppercase() {
-    assertRegular("C:/", "c:/");
+    assertThat(create("C:/")).isEqualTo(create("c:/"));
   }
 
   @Test
   public void testRedundantSlashesWindows() {
-    assertRegular("C:/", "C:///");
-    assertRegular("C:/foo/bar", "C:/foo///bar");
-    assertRegular("C:/foo/bar", "C:////foo//bar");
+    assertThat(create("C:/")).isEqualTo(create("C:///"));
+    assertThat(create("C:/foo/bar")).isEqualTo(create("C:/foo///bar"));
+    assertThat(create("C:/foo/bar")).isEqualTo(create("C:////foo//bar"));
   }
 
   @Test
   public void testSimpleNameToSimpleNameWindows() {
-    assertRegular("C:/foo", "C:/foo");
+    assertThat(create("C:/foo")).isEqualTo(create("C:/foo"));
   }
 
   @Test
   public void testStripsTrailingSlashWindows() {
-    assertRegular("C:/foo/bar", "C:/foo/bar/");
+    assertThat(create("C:/foo/bar")).isEqualTo(create("C:/foo/bar/"));
   }
 
   @Test
   public void testGetParentDirectoryWindows() {
-    PathFragment fooBarWizAbs = PathFragment.create("C:/foo/bar/wiz");
-    PathFragment fooBarAbs = PathFragment.create("C:/foo/bar");
-    PathFragment fooAbs = PathFragment.create("C:/foo");
-    PathFragment rootAbs = PathFragment.create("C:/");
-    assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs);
-    assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs);
-    assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs);
-    assertThat(rootAbs.getParentDirectory()).isNull();
-
-    // Note, this is suprising but correct behaviour:
-    assertThat(PathFragment.create("C:/foo/bar/..").getParentDirectory()).isEqualTo(fooBarAbs);
+    assertThat(create("C:/foo/bar/wiz").getParentDirectory()).isEqualTo(create("C:/foo/bar"));
+    assertThat(create("C:/foo/bar").getParentDirectory()).isEqualTo(create("C:/foo"));
+    assertThat(create("C:/foo").getParentDirectory()).isEqualTo(create("C:/"));
+    assertThat(create("C:/").getParentDirectory()).isNull();
   }
 
   @Test
   public void testSegmentsCountWindows() {
-    assertThat(PathFragment.create("C:/foo").segmentCount()).isEqualTo(1);
-    assertThat(PathFragment.create("C:/").segmentCount()).isEqualTo(0);
+    assertThat(create("C:/foo").segmentCount()).isEqualTo(1);
+    assertThat(create("C:/").segmentCount()).isEqualTo(0);
   }
 
   @Test
   public void testGetSegmentWindows() {
-    assertThat(PathFragment.create("C:/foo/bar").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("C:/foo/bar").getSegment(1)).isEqualTo("bar");
-    assertThat(PathFragment.create("C:/foo/").getSegment(0)).isEqualTo("foo");
-    assertThat(PathFragment.create("C:/foo").getSegment(0)).isEqualTo("foo");
+    assertThat(create("C:/foo/bar").getSegment(0)).isEqualTo("foo");
+    assertThat(create("C:/foo/bar").getSegment(1)).isEqualTo("bar");
+    assertThat(create("C:/foo/").getSegment(0)).isEqualTo("foo");
+    assertThat(create("C:/foo").getSegment(0)).isEqualTo("foo");
   }
 
   @Test
   public void testBasenameWindows() throws Exception {
-    assertThat(PathFragment.create("C:/foo/bar").getBaseName()).isEqualTo("bar");
-    assertThat(PathFragment.create("C:/foo").getBaseName()).isEqualTo("foo");
+    assertThat(create("C:/foo/bar").getBaseName()).isEqualTo("bar");
+    assertThat(create("C:/foo").getBaseName()).isEqualTo("foo");
     // Never return the drive name as a basename.
-    assertThat(PathFragment.create("C:/").getBaseName()).isEmpty();
-  }
-
-  private static void assertPath(String expected, PathFragment actual) {
-    assertThat(actual.getPathString()).isEqualTo(expected);
+    assertThat(create("C:/").getBaseName()).isEmpty();
   }
 
   @Test
   public void testReplaceNameWindows() throws Exception {
-    assertPath("C:/foo/baz", PathFragment.create("C:/foo/bar").replaceName("baz"));
-    assertThat(PathFragment.create("C:/").replaceName("baz")).isNull();
+    assertThat(create("C:/foo/bar").replaceName("baz").getPathString()).isEqualTo("C:/foo/baz");
+    assertThat(create("C:/").replaceName("baz")).isNull();
   }
 
   @Test
   public void testStartsWithWindows() {
-    assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:/foo")))
-        .isTrue();
-    assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:/"))).isTrue();
-    assertThat(PathFragment.create("C:/").startsWith(PathFragment.create("C:/"))).isTrue();
+    assertThat(create("C:/foo/bar").startsWith(create("C:/foo"))).isTrue();
+    assertThat(create("C:/foo/bar").startsWith(create("C:/"))).isTrue();
+    assertThat(create("C:/").startsWith(create("C:/"))).isTrue();
 
     // The first path is absolute, the second is not.
-    assertThat(PathFragment.create("C:/foo/bar").startsWith(PathFragment.create("C:"))).isFalse();
-    assertThat(PathFragment.create("C:/").startsWith(PathFragment.create("C:"))).isFalse();
+    assertThat(create("C:/foo/bar").startsWith(create("C:"))).isFalse();
+    assertThat(create("C:/").startsWith(create("C:"))).isFalse();
   }
 
   @Test
   public void testEndsWithWindows() {
-    assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("bar"))).isTrue();
-    assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("foo/bar"))).isTrue();
-    assertThat(PathFragment.create("C:/foo/bar").endsWith(PathFragment.create("C:/foo/bar")))
-        .isTrue();
-    assertThat(PathFragment.create("C:/").endsWith(PathFragment.create("C:/"))).isTrue();
+    assertThat(create("C:/foo/bar").endsWith(create("bar"))).isTrue();
+    assertThat(create("C:/foo/bar").endsWith(create("foo/bar"))).isTrue();
+    assertThat(create("C:/foo/bar").endsWith(create("C:/foo/bar"))).isTrue();
+    assertThat(create("C:/").endsWith(create("C:/"))).isTrue();
   }
 
   @Test
   public void testGetSafePathStringWindows() {
-    assertThat(PathFragment.create("C:/").getSafePathString()).isEqualTo("C:/");
-    assertThat(PathFragment.create("C:/abc").getSafePathString()).isEqualTo("C:/abc");
-    assertThat(PathFragment.create("C:/abc/def").getSafePathString()).isEqualTo("C:/abc/def");
+    assertThat(create("C:/").getSafePathString()).isEqualTo("C:/");
+    assertThat(create("C:/abc").getSafePathString()).isEqualTo("C:/abc");
+    assertThat(create("C:/abc/def").getSafePathString()).isEqualTo("C:/abc/def");
   }
 
   @Test
   public void testNormalizeWindows() {
-    assertThat(PathFragment.create("C:/a/b").normalize()).isEqualTo(PathFragment.create("C:/a/b"));
-    assertThat(PathFragment.create("C:/a/./b").normalize())
-        .isEqualTo(PathFragment.create("C:/a/b"));
-    assertThat(PathFragment.create("C:/a/../b").normalize()).isEqualTo(PathFragment.create("C:/b"));
-    assertThat(PathFragment.create("C:/../b").normalize())
-        .isEqualTo(PathFragment.create("C:/../b"));
+    assertThat(create("C:/a/b")).isEqualTo(create("C:/a/b"));
+    assertThat(create("C:/a/./b")).isEqualTo(create("C:/a/b"));
+    assertThat(create("C:/a/../b")).isEqualTo(create("C:/b"));
+    assertThat(create("C:/../b")).isEqualTo(create("C:/../b"));
   }
 
   @Test
@@ -335,8 +231,21 @@
     // of drive C:\".
     // Bazel doesn't resolve such paths, and just takes them literally like normal path segments.
     // If the user attempts to open files under such paths, the file system API will give an error.
-    assertThat(PathFragment.create("C:").isAbsolute()).isFalse();
-    assertThat(PathFragment.create("C:").getDriveLetter()).isEqualTo('\0');
-    assertThat(PathFragment.create("C:").getSegments()).containsExactly("C:");
+    assertThat(create("C:").isAbsolute()).isFalse();
+    assertThat(create("C:").getSegments()).containsExactly("C:");
+  }
+
+  @Test
+  public void testToRelative() {
+    assertThat(create("C:/foo/bar").toRelative()).isEqualTo(create("foo/bar"));
+    assertThat(create("C:/").toRelative()).isEqualTo(create(""));
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").toRelative());
+  }
+
+  @Test
+  public void testGetDriveStr() {
+    assertThat(create("C:/foo/bar").getDriveStr()).isEqualTo("C:/");
+    assertThat(create("C:/").getDriveStr()).isEqualTo("C:/");
+    MoreAsserts.expectThrows(IllegalArgumentException.class, () -> create("foo").getDriveStr());
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java
deleted file mode 100644
index eaad458..0000000
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathTest.java
+++ /dev/null
@@ -1,341 +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 static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.testing.EqualsTester;
-import com.google.common.testing.GcFinalization;
-import com.google.devtools.build.lib.clock.BlazeClock;
-import com.google.devtools.build.lib.skyframe.serialization.InjectingObjectCodecAdapter;
-import com.google.devtools.build.lib.skyframe.serialization.testutils.FsUtils;
-import com.google.devtools.build.lib.skyframe.serialization.testutils.ObjectCodecTester;
-import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.lang.ref.WeakReference;
-import java.net.URI;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * A test for {@link Path}.
- */
-@RunWith(JUnit4.class)
-public class PathTest {
-  private FileSystem filesystem;
-  private Path root;
-
-  @Before
-  public final void initializeFileSystem() throws Exception  {
-    filesystem = new InMemoryFileSystem(BlazeClock.instance());
-    root = filesystem.getRootDirectory();
-    Path first = root.getChild("first");
-    first.createDirectory();
-  }
-
-  @Test
-  public void testStartsWithWorksForSelf() {
-    assertStartsWithReturns(true, "/first/child", "/first/child");
-  }
-
-  @Test
-  public void testStartsWithWorksForChild() {
-    assertStartsWithReturns(true,
-        "/first/child", "/first/child/grandchild");
-  }
-
-  @Test
-  public void testStartsWithWorksForDeepDescendant() {
-    assertStartsWithReturns(true,
-        "/first/child", "/first/child/grandchild/x/y/z");
-  }
-
-  @Test
-  public void testStartsWithFailsForParent() {
-    assertStartsWithReturns(false, "/first/child", "/first");
-  }
-
-  @Test
-  public void testStartsWithFailsForSibling() {
-    assertStartsWithReturns(false, "/first/child", "/first/child2");
-  }
-
-  @Test
-  public void testStartsWithFailsForLinkToDescendant()
-      throws Exception {
-    Path linkTarget = filesystem.getPath("/first/linked_to");
-    FileSystemUtils.createEmptyFile(linkTarget);
-    Path second = filesystem.getPath("/second/");
-    second.createDirectory();
-    second.getChild("child_link").createSymbolicLink(linkTarget);
-    assertStartsWithReturns(false, "/first", "/second/child_link");
-  }
-
-  @Test
-  public void testStartsWithFailsForNullPrefix() {
-    try {
-      filesystem.getPath("/first").startsWith(null);
-      fail();
-    } catch (Exception e) {
-    }
-  }
-
-  private void assertStartsWithReturns(boolean expected,
-                                       String ancestor,
-                                       String descendant) {
-    Path parent = filesystem.getPath(ancestor);
-    Path child = filesystem.getPath(descendant);
-    assertThat(child.startsWith(parent)).isEqualTo(expected);
-  }
-
-  @Test
-  public void testGetChildWorks() {
-    assertGetChildWorks("second");
-    assertGetChildWorks("...");
-    assertGetChildWorks("....");
-  }
-
-  private void assertGetChildWorks(String childName) {
-    assertThat(filesystem.getPath("/first").getChild(childName))
-        .isEqualTo(filesystem.getPath("/first/" + childName));
-  }
-
-  @Test
-  public void testGetChildFailsForChildWithSlashes() {
-    assertGetChildFails("second/third");
-    assertGetChildFails("./third");
-    assertGetChildFails("../third");
-    assertGetChildFails("second/..");
-    assertGetChildFails("second/.");
-    assertGetChildFails("/third");
-    assertGetChildFails("third/");
-  }
-
-  private void assertGetChildFails(String childName) {
-    try {
-      filesystem.getPath("/first").getChild(childName);
-      fail();
-    } catch (IllegalArgumentException e) {
-      // Expected.
-    }
-  }
-
-  @Test
-  public void testGetChildFailsForDotAndDotDot() {
-    assertGetChildFails(".");
-    assertGetChildFails("..");
-  }
-
-  @Test
-  public void testGetChildFailsForEmptyString() {
-    assertGetChildFails("");
-  }
-
-  @Test
-  public void testRelativeToWorks() {
-    assertRelativeToWorks("apple", "/fruit/apple", "/fruit");
-    assertRelativeToWorks("apple/jonagold", "/fruit/apple/jonagold", "/fruit");
-  }
-
-  @Test
-  public void testGetRelativeWithStringWorks() {
-    assertGetRelativeWorks("/first/x/y", "y");
-    assertGetRelativeWorks("/y", "/y");
-    assertGetRelativeWorks("/first/x/x", "./x");
-    assertGetRelativeWorks("/first/y", "../y");
-    assertGetRelativeWorks("/", "../../../../..");
-  }
-
-  @Test
-  public void testAsFragmentWorks() {
-    assertAsFragmentWorks("/");
-    assertAsFragmentWorks("//");
-    assertAsFragmentWorks("/first");
-    assertAsFragmentWorks("/first/x/y");
-    assertAsFragmentWorks("/first/x/y.foo");
-  }
-
-  @Test
-  public void testGetRelativeWithFragmentWorks() {
-    Path dir = filesystem.getPath("/first/x");
-    assertThat(dir.getRelative(PathFragment.create("y")).toString()).isEqualTo("/first/x/y");
-    assertThat(dir.getRelative(PathFragment.create("./x")).toString()).isEqualTo("/first/x/x");
-    assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("/first/y");
-  }
-
-  @Test
-  public void testGetRelativeWithAbsoluteFragmentWorks() {
-    Path root = filesystem.getPath("/first/x");
-    assertThat(root.getRelative(PathFragment.create("/x/y")).toString()).isEqualTo("/x/y");
-  }
-
-  @Test
-  public void testGetRelativeWithAbsoluteStringWorks() {
-    Path root = filesystem.getPath("/first/x");
-    assertThat(root.getRelative("/x/y").toString()).isEqualTo("/x/y");
-  }
-
-  @Test
-  public void testComparableSortOrder() {
-    Path zzz = filesystem.getPath("/zzz");
-    Path ZZZ = filesystem.getPath("/ZZZ");
-    Path abc = filesystem.getPath("/abc");
-    Path aBc = filesystem.getPath("/aBc");
-    Path AbC = filesystem.getPath("/AbC");
-    Path ABC = filesystem.getPath("/ABC");
-    List<Path> list = Lists.newArrayList(zzz, ZZZ, ABC, aBc, AbC, abc);
-    Collections.sort(list);
-    assertThat(list).containsExactly(ABC, AbC, ZZZ, aBc, abc, zzz).inOrder();
-  }
-
-  @Test
-  public void testParentOfRootIsRoot() {
-    assertThat(root.getRelative("..")).isSameAs(root);
-
-    assertThat(root.getRelative("broken/../../dots")).isSameAs(root.getRelative("dots"));
-  }
-
-  @Test
-  public void testSingleSegmentEquivalence() {
-    assertThat(root.getRelative("aSingleSegment")).isSameAs(root.getRelative("aSingleSegment"));
-  }
-
-  @Test
-  public void testSiblingNonEquivalenceString() {
-    assertThat(root.getRelative("aDifferentSegment"))
-        .isNotSameAs(root.getRelative("aSingleSegment"));
-  }
-
-  @Test
-  public void testSiblingNonEquivalenceFragment() {
-    assertThat(root.getRelative(PathFragment.create("aDifferentSegment")))
-        .isNotSameAs(root.getRelative(PathFragment.create("aSingleSegment")));
-  }
-
-  @Test
-  public void testHashCodeStableAcrossGarbageCollections() {
-    Path parent = filesystem.getPath("/a");
-    PathFragment childFragment = PathFragment.create("b");
-    Path child = parent.getRelative(childFragment);
-    WeakReference<Path> childRef = new WeakReference<>(child);
-    int childHashCode1 = childRef.get().hashCode();
-    assertThat(parent.getRelative(childFragment).hashCode()).isEqualTo(childHashCode1);
-    child = null;
-    GcFinalization.awaitClear(childRef);
-    int childHashCode2 = parent.getRelative(childFragment).hashCode();
-    assertThat(childHashCode2).isEqualTo(childHashCode1);
-  }
-
-  @Test
-  public void testSerialization() throws Exception {
-    FileSystem oldFileSystem = Path.getFileSystemForSerialization();
-    try {
-      Path.setFileSystemForSerialization(filesystem);
-      Path root = filesystem.getPath("/");
-      Path p1 = filesystem.getPath("/foo");
-      Path p2 = filesystem.getPath("/foo/bar");
-
-      ByteArrayOutputStream bos = new ByteArrayOutputStream();
-      ObjectOutputStream oos = new ObjectOutputStream(bos);
-
-      oos.writeObject(root);
-      oos.writeObject(p1);
-      oos.writeObject(p2);
-
-      ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
-      ObjectInputStream ois = new ObjectInputStream(bis);
-
-      Path dsRoot = (Path) ois.readObject();
-      Path dsP1 = (Path) ois.readObject();
-      Path dsP2 = (Path) ois.readObject();
-
-      new EqualsTester()
-          .addEqualityGroup(root, dsRoot)
-          .addEqualityGroup(p1, dsP1)
-          .addEqualityGroup(p2, dsP2)
-          .testEquals();
-
-      assertThat(p2.startsWith(p1)).isTrue();
-      assertThat(p2.startsWith(dsP1)).isTrue();
-      assertThat(dsP2.startsWith(p1)).isTrue();
-      assertThat(dsP2.startsWith(dsP1)).isTrue();
-
-      // Regression test for a very specific bug in compareTo involving our incorrect usage of
-      // reference equality rather than logical equality.
-      String relativePathStringA = "child/grandchildA";
-      String relativePathStringB = "child/grandchildB";
-      assertThat(
-              p1.getRelative(relativePathStringA).compareTo(dsP1.getRelative(relativePathStringB)))
-          .isEqualTo(
-              p1.getRelative(relativePathStringA).compareTo(p1.getRelative(relativePathStringB)));
-    } finally {
-      Path.setFileSystemForSerialization(oldFileSystem);
-    }
-  }
-
-  @Test
-  public void testAbsolutePathRoot() {
-    assertThat(new Path(null).toString()).isEqualTo("/");
-  }
-
-  @Test
-  public void testAbsolutePath() {
-    Path segment = new Path(null, "bar.txt",
-      new Path(null, "foo", new Path(null)));
-    assertThat(segment.toString()).isEqualTo("/foo/bar.txt");
-  }
-
-  @Test
-  public void testToURI() throws Exception {
-    Path p = root.getRelative("/tmp/foo bar.txt");
-    URI uri = p.toURI();
-    assertThat(uri.toString()).isEqualTo("file:///tmp/foo%20bar.txt");
-  }
-
-  @Test
-  public void testCodec() throws Exception {
-    ObjectCodecTester.newBuilder(
-            new InjectingObjectCodecAdapter<>(Path.CODEC, FsUtils.TEST_FILESYSTEM_PROVIDER))
-        .addSubjects(
-            ImmutableList.of(
-                FsUtils.TEST_FILESYSTEM.getPath("/"),
-                FsUtils.TEST_FILESYSTEM.getPath("/some/path"),
-                FsUtils.TEST_FILESYSTEM.getPath("/some/other/path/with/empty/last/fragment/")))
-        .buildAndRunTests();
-  }
-
-  private void assertAsFragmentWorks(String expected) {
-    assertThat(filesystem.getPath(expected).asFragment()).isEqualTo(PathFragment.create(expected));
-  }
-
-  private void assertGetRelativeWorks(String expected, String relative) {
-    assertThat(filesystem.getPath("/first/x").getRelative(relative))
-        .isEqualTo(filesystem.getPath(expected));
-  }
-
-  private void assertRelativeToWorks(String expected, String relative, String original) {
-    assertThat(filesystem.getPath(relative).relativeTo(filesystem.getPath(original)))
-        .isEqualTo(PathFragment.create(expected));
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java b/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java
deleted file mode 100644
index 0807b4a..0000000
--- a/src/test/java/com/google/devtools/build/lib/vfs/PathTrieTest.java
+++ /dev/null
@@ -1,78 +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 static com.google.common.truth.Truth.assertThat;
-import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Unit tests for {@link PathTrie}. */
-@RunWith(JUnit4.class)
-public class PathTrieTest {
-  @Test
-  public void empty() {
-    PathTrie<Integer> pathTrie = new PathTrie<>();
-    assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/x"))).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/x/y"))).isNull();
-  }
-
-  @Test
-  public void simpleBranches() {
-    PathTrie<Integer> pathTrie = new PathTrie<>();
-    pathTrie.put(PathFragment.create("/a"), 1);
-    pathTrie.put(PathFragment.create("/b"), 2);
-
-    assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/b"))).isEqualTo(2);
-    assertThat(pathTrie.get(PathFragment.create("/b/c"))).isEqualTo(2);
-  }
-
-  @Test
-  public void nestedDirectories() {
-    PathTrie<Integer> pathTrie = new PathTrie<>();
-    pathTrie.put(PathFragment.create("/a/b/c"), 3);
-    assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/a"))).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/a/b"))).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(3);
-    assertThat(pathTrie.get(PathFragment.create("/a/b/c/d"))).isEqualTo(3);
-
-    pathTrie.put(PathFragment.create("/a"), 1);
-    assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/b"))).isNull();
-    assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/a/b/c"))).isEqualTo(3);
-    assertThat(pathTrie.get(PathFragment.create("/a/b/c/d"))).isEqualTo(3);
-
-    pathTrie.put(PathFragment.ROOT_FRAGMENT, 0);
-    assertThat(pathTrie.get(PathFragment.EMPTY_FRAGMENT)).isEqualTo(0);
-    assertThat(pathTrie.get(PathFragment.create("/b"))).isEqualTo(0);
-    assertThat(pathTrie.get(PathFragment.create("/a"))).isEqualTo(1);
-    assertThat(pathTrie.get(PathFragment.create("/a/b"))).isEqualTo(1);
-  }
-
-  @Test
-  public void unrootedDirectoriesError() {
-    PathTrie<Integer> pathTrie = new PathTrie<>();
-    assertThrows(IllegalArgumentException.class, () -> pathTrie.put(PathFragment.create("a"), 1));
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java
index 4720a6e..ba7029d 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/SymlinkAwareFileSystemTest.java
@@ -16,6 +16,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.fail;
 
+import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -197,7 +198,8 @@
       linkPath.delete();
       createSymbolicLink(linkPath, relative);
       if (testFS.supportsSymbolicLinksNatively(linkPath)) {
-        assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW)).isEqualTo(linkTarget.length());
+        assertThat(linkPath.getFileSize(Symlinks.NOFOLLOW))
+            .isEqualTo(relative.getSafePathString().length());
         assertThat(linkPath.readSymbolicLink()).isEqualTo(relative);
       }
     }
@@ -205,6 +207,10 @@
 
   @Test
   public void testLinkToRootResolvesCorrectly() throws IOException {
+    if (OS.getCurrent() == OS.WINDOWS) {
+      // This test cannot be run on Windows, it mixes "/" paths with "C:/" paths
+      return;
+    }
     Path rootPath = testFS.getPath("/");
 
     try {
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java
index 1e877a5..3799cfd 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnionFileSystemTest.java
@@ -163,7 +163,7 @@
 
     // FileSystemTest.setUp() silently creates the test root on the filesystem...
     Path testDirUnderRoot = unionfs.getPath(workingDir.asFragment().subFragment(0, 1));
-    assertThat(unionfs.getDirectoryEntries(unionfs.getRootDirectory()))
+    assertThat(unionfs.getDirectoryEntries(unionfs.getPath("/")))
         .containsExactly(
             foo.getBaseName(),
             bar.getBaseName(),
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
similarity index 76%
rename from src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java
rename to src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
index 2cdb401..306ca9b 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/UnixLocalPathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/UnixPathTest.java
@@ -17,15 +17,13 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.testing.EqualsTester;
-import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
-import com.google.devtools.build.lib.vfs.LocalPath.UnixOsPathPolicy;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests the unix implementation of {@link LocalPath}. */
+/** Tests the unix implementation of {@link Path}. */
 @RunWith(JUnit4.class)
-public class UnixLocalPathTest extends LocalPathAbstractTest {
+public class UnixPathTest extends PathAbstractTest {
 
   @Test
   public void testEqualsAndHashCodeUnix() {
@@ -37,8 +35,6 @@
 
   @Test
   public void testRelativeToUnix() {
-    // Cannot relativize absolute and non-absolute
-    assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
     assertThat(create("/").relativeTo(create("/")).getPathString()).isEmpty();
     assertThat(create("/foo").relativeTo(create("/foo")).getPathString()).isEmpty();
     assertThat(create("/foo/bar/baz").relativeTo(create("/foo")).getPathString())
@@ -53,16 +49,24 @@
   }
 
   @Test
-  public void testIsAbsoluteUnix() {
-    assertThat(create("/absolute/test").isAbsolute()).isTrue();
-    assertThat(create("relative/test").isAbsolute()).isFalse();
-  }
-
-  @Test
   public void testGetRelativeUnix() {
     assertThat(create("/a").getRelative("b").getPathString()).isEqualTo("/a/b");
+    assertThat(create("/a/b").getRelative("c/d").getPathString()).isEqualTo("/a/b/c/d");
+    assertThat(create("/c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
+    assertThat(create("/a").getRelative("").getPathString()).isEqualTo("/a");
     assertThat(create("/").getRelative("").getPathString()).isEqualTo("/");
-    assertThat(create("c/d").getRelative("/a/b").getPathString()).isEqualTo("/a/b");
+    assertThat(create("/a/b").getRelative("../foo").getPathString()).isEqualTo("/a/foo");
+
+    // Make sure any fast path of Path#getRelative(PathFragment) works
+    assertThat(create("/a/b").getRelative(PathFragment.create("../foo")).getPathString())
+        .isEqualTo("/a/foo");
+
+    // Make sure any fast path of Path#getRelative(PathFragment) works
+    assertThat(create("/c/d").getRelative(PathFragment.create("/a/b")).getPathString())
+        .isEqualTo("/a/b");
+
+    // Test normalization
+    assertThat(create("/a").getRelative(".").getPathString()).isEqualTo("/a");
   }
 
   @Test
@@ -107,14 +111,10 @@
 
   @Test
   public void testGetParentDirectoryUnix() {
-    LocalPath fooBarWizAbs = create("/foo/bar/wiz");
-    LocalPath fooBarAbs = create("/foo/bar");
-    LocalPath fooAbs = create("/foo");
-    LocalPath rootAbs = create("/");
-    assertThat(fooBarWizAbs.getParentDirectory()).isEqualTo(fooBarAbs);
-    assertThat(fooBarAbs.getParentDirectory()).isEqualTo(fooAbs);
-    assertThat(fooAbs.getParentDirectory()).isEqualTo(rootAbs);
-    assertThat(rootAbs.getParentDirectory()).isNull();
+    assertThat(create("/foo/bar/wiz").getParentDirectory()).isEqualTo(create("/foo/bar"));
+    assertThat(create("/foo/bar").getParentDirectory()).isEqualTo(create("/foo"));
+    assertThat(create("/foo").getParentDirectory()).isEqualTo(create("/"));
+    assertThat(create("/").getParentDirectory()).isNull();
   }
 
   @Test
@@ -127,8 +127,7 @@
 
   @Test
   public void testStartsWithUnix() {
-    LocalPath foobar = create("/foo/bar");
-    LocalPath foobarRelative = create("foo/bar");
+    Path foobar = create("/foo/bar");
 
     // (path, prefix) => true
     assertThat(foobar.startsWith(foobar)).isTrue();
@@ -141,13 +140,6 @@
     assertThat(create("/foo").startsWith(foobar)).isFalse();
     assertThat(create("/").startsWith(foobar)).isFalse();
 
-    // (absolute, relative) => false
-    assertThat(foobar.startsWith(foobarRelative)).isFalse();
-    assertThat(foobarRelative.startsWith(foobar)).isFalse();
-
-    // relative paths start with nothing, absolute paths do not
-    assertThat(foobar.startsWith(create(""))).isFalse();
-
     // (path, sibling) => false
     assertThat(create("/foo/wiz").startsWith(foobar)).isFalse();
     assertThat(foobar.startsWith(create("/foo/wiz"))).isFalse();
@@ -162,8 +154,10 @@
     assertThat(create("/..")).isEqualTo(create("/.."));
   }
 
-  @Override
-  protected OsPathPolicy getFilePathOs() {
-    return new UnixOsPathPolicy();
+  @Test
+  public void testParentOfRootIsRootUnix() {
+    assertThat(create("/..")).isEqualTo(create("/"));
+    assertThat(create("/../../../../../..")).isEqualTo(create("/"));
+    assertThat(create("/../../../foo")).isEqualTo(create("/foo"));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java b/src/test/java/com/google/devtools/build/lib/vfs/WindowsPathTest.java
similarity index 62%
rename from src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java
rename to src/test/java/com/google/devtools/build/lib/vfs/WindowsPathTest.java
index ac5acef..c759219 100644
--- a/src/test/java/com/google/devtools/build/lib/vfs/WindowsLocalPathTest.java
+++ b/src/test/java/com/google/devtools/build/lib/vfs/WindowsPathTest.java
@@ -17,18 +17,16 @@
 
 import com.google.common.testing.EqualsTester;
 import com.google.devtools.build.lib.testutil.MoreAsserts;
-import com.google.devtools.build.lib.vfs.LocalPath.OsPathPolicy;
-import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy;
-import com.google.devtools.build.lib.vfs.LocalPath.WindowsOsPathPolicy.ShortPathResolver;
+import com.google.devtools.build.lib.vfs.WindowsOsPathPolicy.ShortPathResolver;
 import java.util.HashMap;
 import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests windows-specific parts of {@link LocalPath} */
+/** Tests windows-specific parts of {@link Path} */
 @RunWith(JUnit4.class)
-public class WindowsLocalPathTest extends LocalPathAbstractTest {
+public class WindowsPathTest extends PathAbstractTest {
 
   private static final class MockShortPathResolver implements ShortPathResolver {
     // Full path to resolved child mapping.
@@ -52,26 +50,18 @@
     }
   }
 
-  private final MockShortPathResolver shortPathResolver = new MockShortPathResolver();
-
-  @Override
-  protected OsPathPolicy getFilePathOs() {
-    return new WindowsOsPathPolicy(shortPathResolver);
-  }
-
   @Test
   public void testEqualsAndHashcodeWindows() {
     new EqualsTester()
-        .addEqualityGroup(create("a/b"), create("A/B"))
         .addEqualityGroup(create("/a/b"), create("/A/B"))
         .addEqualityGroup(create("c:/a/b"), create("C:\\A\\B"))
-        .addEqualityGroup(create("something/else"))
+        .addEqualityGroup(create("C:/something/else"))
         .testEquals();
   }
 
   @Test
   public void testCaseIsPreserved() {
-    assertThat(create("a/B").getPathString()).isEqualTo("a/B");
+    assertThat(create("C:/a/B").getPathString()).isEqualTo("C:/a/B");
   }
 
   @Test
@@ -96,69 +86,62 @@
   public void testGetParentDirectoryWindows() {
     assertThat(create("C:/foo").getParentDirectory()).isEqualTo(create("C:/"));
     assertThat(create("C:/").getParentDirectory()).isNull();
+    assertThat(create("/").getParentDirectory()).isNull();
   }
 
   @Test
-  public void testisAbsoluteWindows() {
-    assertThat(create("C:/").isAbsolute()).isTrue();
-    // test that msys paths turn into absolute paths
-    assertThat(create("/").isAbsolute()).isTrue();
+  public void testParentOfRootIsRootWindows() {
+    assertThat(create("C:/..")).isEqualTo(create("C:/"));
+    assertThat(create("C:/../../../../../..")).isEqualTo(create("C:/"));
+    assertThat(create("C:/../../../foo")).isEqualTo(create("C:/foo"));
   }
 
   @Test
   public void testRelativeToWindows() {
-    assertThat(create("C:/foo").relativeTo(create("C:/"))).isEqualTo(create("foo"));
+    assertThat(create("C:/foo").relativeTo(create("C:/")).getPathString()).isEqualTo("foo");
     // Case insensitivity test
-    assertThat(create("C:/foo/bar").relativeTo(create("C:/FOO"))).isEqualTo(create("bar"));
+    assertThat(create("C:/foo/bar").relativeTo(create("C:/FOO")).getPathString()).isEqualTo("bar");
     MoreAsserts.assertThrows(
         IllegalArgumentException.class, () -> create("D:/foo").relativeTo(create("C:/")));
   }
 
   @Test
-  public void testAbsoluteUnixPathIsRelativeToWindowsUnixRoot() {
-    assertThat(create("/").getPathString()).isEqualTo("C:/fake/msys");
-    assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar");
-    assertThat(create("/foo/bar").getPathString()).isEqualTo("C:/fake/msys/foo/bar");
-  }
-
-  @Test
-  public void testAbsoluteUnixPathReferringToDriveIsRecognized() {
-    assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo");
-    assertThat(create("/c/foo").getPathString()).isEqualTo("C:/foo");
-    assertThat(create("/c:").getPathString()).isNotEqualTo("C:/foo");
-  }
-
-  @Test
   public void testResolvesShortenedPaths() {
+    MockShortPathResolver shortPathResolver = new MockShortPathResolver();
+    WindowsOsPathPolicy osPathPolicy = new WindowsOsPathPolicy(shortPathResolver);
     shortPathResolver.resolutions.put("d:/progra~1", "program files");
     shortPathResolver.resolutions.put("d:/program files/micros~1", "microsoft something");
     shortPathResolver.resolutions.put(
         "d:/program files/microsoft something/foo/~bar~1", "~bar_hello");
 
     // Assert normal shortpath resolution.
-    LocalPath normal = create("d:/progra~1/micros~1/foo/~bar~1/baz");
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(normal.getPathString())
+    assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/~bar~1/baz"))
         .isEqualTo("D:/program files/microsoft something/foo/~bar_hello/baz");
-    LocalPath notYetExistent = create("d:/progra~1/micros~1/foo/will~1.exi/bar");
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(notYetExistent.getPathString())
+    assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/will~1.exi/bar"))
         .isEqualTo("D:/program files/microsoft something/foo/will~1.exi/bar");
 
-    LocalPath msRoot = create("d:/progra~1/micros~1");
-    assertThat(msRoot.getPathString()).isEqualTo("D:/program files/microsoft something");
+    assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1"))
+        .isEqualTo("D:/program files/microsoft something");
 
     // Pretend that a path we already failed to resolve once came into existence.
     shortPathResolver.resolutions.put(
         "d:/program files/microsoft something/foo/will~1.exi", "will.exist");
 
     // Assert that this time we can resolve the previously non-existent path.
-    LocalPath nowExists = create("d:/progra~1/micros~1/foo/will~1.exi/bar");
     // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(nowExists.getPathString())
+    assertThat(normalize(osPathPolicy, "d:/progra~1/micros~1/foo/will~1.exi/bar"))
         .isEqualTo("D:/program files/microsoft something/foo/will.exist/bar");
 
-    // Assert relative paths that look like short paths are untouched
-    assertThat(create("progra~1").getPathString()).isEqualTo("progra~1");
+    // Check needsToNormalized
+    assertThat(osPathPolicy.needsToNormalize("d:/progra~1/micros~1/foo/will~1.exi/bar"))
+        .isEqualTo(WindowsOsPathPolicy.NEEDS_SHORT_PATH_NORMALIZATION);
+    assertThat(osPathPolicy.needsToNormalize("will~1.exi"))
+        .isEqualTo(WindowsOsPathPolicy.NEEDS_SHORT_PATH_NORMALIZATION);
+    assertThat(osPathPolicy.needsToNormalize("d:/no-normalization"))
+        .isEqualTo(WindowsOsPathPolicy.NORMALIZED); // Sanity check
+  }
+
+  private static String normalize(OsPathPolicy osPathPolicy, String str) {
+    return osPathPolicy.normalize(str, osPathPolicy.needsToNormalize(str));
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java b/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java
deleted file mode 100644
index e77de52..0000000
--- a/src/test/java/com/google/devtools/build/lib/windows/PathWindowsTest.java
+++ /dev/null
@@ -1,298 +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.windows;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.base.Function;
-import com.google.common.base.Predicate;
-import com.google.devtools.build.lib.clock.BlazeClock;
-import com.google.devtools.build.lib.vfs.FileSystem;
-import com.google.devtools.build.lib.vfs.Path;
-import com.google.devtools.build.lib.vfs.Path.PathFactory;
-import com.google.devtools.build.lib.vfs.PathFragment;
-import com.google.devtools.build.lib.vfs.Root;
-import com.google.devtools.build.lib.vfs.RootedPath;
-import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** A test for windows aspects of {@link Path}. */
-@RunWith(JUnit4.class)
-public class PathWindowsTest {
-
-  private static final class MockShortPathResolver implements Function<String, String> {
-    public List<String> resolutionQueries = new ArrayList<>();
-
-    // Full path to resolved child mapping.
-    public Map<String, String> resolutions = new HashMap<>();
-
-    @Override
-    public String apply(String path) {
-      path = path.toLowerCase();
-      resolutionQueries.add(path);
-      return resolutions.get(path);
-    }
-  }
-
-  private FileSystem filesystem;
-  private WindowsPath root;
-  private final MockShortPathResolver shortPathResolver = new MockShortPathResolver();
-
-  @Before
-  public final void initializeFileSystem() throws Exception {
-    filesystem =
-        new InMemoryFileSystem(BlazeClock.instance()) {
-          @Override
-          protected PathFactory getPathFactory() {
-            return WindowsFileSystem.getPathFactoryForTesting(shortPathResolver);
-          }
-
-          @Override
-          public boolean isFilePathCaseSensitive() {
-            return false;
-          }
-        };
-    root = (WindowsPath) filesystem.getRootDirectory().getRelative("C:/");
-    root.createDirectory();
-  }
-
-  private void assertAsFragmentWorks(String expected) {
-    assertThat(filesystem.getPath(expected).asFragment()).isEqualTo(PathFragment.create(expected));
-  }
-
-  @Test
-  public void testWindowsPath() {
-    Path p = filesystem.getPath("C:/foo/bar");
-    assertThat(p.getPathString()).isEqualTo("C:/foo/bar");
-    assertThat(p.toString()).isEqualTo("C:/foo/bar");
-  }
-
-  @Test
-  public void testAsFragmentWindows() {
-    assertAsFragmentWorks("C:/");
-    assertAsFragmentWorks("C://");
-    assertAsFragmentWorks("C:/first");
-    assertAsFragmentWorks("C:/first/x/y");
-    assertAsFragmentWorks("C:/first/x/y.foo");
-  }
-
-  @Test
-  public void testGetRelativeWithFragmentWindows() {
-    Path dir = filesystem.getPath("C:/first/x");
-    assertThat(dir.getRelative(PathFragment.create("y")).toString()).isEqualTo("C:/first/x/y");
-    assertThat(dir.getRelative(PathFragment.create("./x")).toString()).isEqualTo("C:/first/x/x");
-    assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("C:/first/y");
-    assertThat(dir.getRelative(PathFragment.create("../y")).toString()).isEqualTo("C:/first/y");
-    assertThat(dir.getRelative(PathFragment.create("../../../y")).toString()).isEqualTo("C:/y");
-  }
-
-  @Test
-  public void testGetRelativeWithAbsoluteFragmentWindows() {
-    Path x = filesystem.getPath("C:/first/x");
-    assertThat(x.getRelative(PathFragment.create("C:/x/y")).toString()).isEqualTo("C:/x/y");
-  }
-
-  @Test
-  public void testGetRelativeWithAbsoluteStringWorksWindows() {
-    Path x = filesystem.getPath("C:/first/x");
-    assertThat(x.getRelative("C:/x/y").toString()).isEqualTo("C:/x/y");
-  }
-
-  @Test
-  public void testParentOfRootIsRootWindows() {
-    assertThat(root).isSameAs(root.getRelative(".."));
-    assertThat(root.getRelative("dots")).isSameAs(root.getRelative("broken/../../dots"));
-  }
-
-  @Test
-  public void testStartsWithWorksOnWindows() {
-    assertStartsWithReturnsOnWindows(true, "C:/first/x", "C:/first/x/y");
-    assertStartsWithReturnsOnWindows(true, "c:/first/x", "C:/FIRST/X/Y");
-    assertStartsWithReturnsOnWindows(true, "C:/FIRST/X", "c:/first/x/y");
-    assertStartsWithReturnsOnWindows(false, "C:/", "/");
-    assertStartsWithReturnsOnWindows(false, "C:/", "D:/");
-    assertStartsWithReturnsOnWindows(false, "C:/", "D:/foo");
-  }
-
-  @Test
-  public void testGetRelative() {
-    Path x = filesystem.getPath("C:\\first\\x");
-    Path other = x.getRelative("a\\b\\c");
-    assertThat(other.asFragment().getPathString()).isEqualTo("C:/first/x/a/b/c");
-  }
-
-  private static void assertStartsWithReturnsOnWindows(
-      boolean expected, String ancestor, String descendant) {
-    FileSystem windowsFileSystem = new WindowsFileSystem();
-    Path parent = windowsFileSystem.getPath(ancestor);
-    Path child = windowsFileSystem.getPath(descendant);
-    assertThat(child.startsWith(parent)).isEqualTo(expected);
-  }
-
-  @Test
-  public void testResolvesShortenedPaths() {
-    shortPathResolver.resolutions.put("d:/progra~1", "program files");
-    shortPathResolver.resolutions.put("d:/program files/micros~1", "microsoft something");
-    shortPathResolver.resolutions.put(
-        "d:/program files/microsoft something/foo/~bar~1", "~bar_hello");
-
-    // Assert normal shortpath resolution.
-    Path normal = root.getRelative("d:/progra~1/micros~1/foo/~bar~1/baz");
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(normal.getPathString())
-        .isEqualTo("D:/program files/microsoft something/foo/~bar_hello/baz");
-    // Assert that we only try to resolve the path segments that look like they may be shortened.
-    assertThat(shortPathResolver.resolutionQueries)
-        .containsExactly(
-            "d:/progra~1",
-            "d:/program files/micros~1",
-            "d:/program files/microsoft something/foo/~bar~1");
-
-    // Assert resolving a path that has a segment which doesn't exist but later will.
-    shortPathResolver.resolutionQueries.clear();
-    Path notYetExistent = root.getRelative("d:/progra~1/micros~1/foo/will~1.exi/bar");
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(notYetExistent.getPathString())
-        .isEqualTo("D:/program files/microsoft something/foo/will~1.exi/bar");
-    // Assert that we only try to resolve the path segments that look like they may be shortened.
-    assertThat(shortPathResolver.resolutionQueries)
-        .containsExactly(
-            "d:/progra~1",
-            "d:/program files/micros~1",
-            "d:/program files/microsoft something/foo/will~1.exi");
-
-    // Assert that the paths we failed to resolve don't get cached.
-    final List<String> children = new ArrayList<>(2);
-    Predicate<Path> collector =
-        new Predicate<Path>() {
-          @Override
-          public boolean apply(Path child) {
-            children.add(child.getPathString());
-            return true;
-          }
-        };
-
-    WindowsPath msRoot = (WindowsPath) root.getRelative("d:/progra~1/micros~1");
-    assertThat(msRoot.getPathString()).isEqualTo("D:/program files/microsoft something");
-    msRoot.applyToChildren(collector);
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(children).containsExactly("D:/program files/microsoft something/foo");
-
-    // Assert that the non-resolvable path was not cached.
-    children.clear();
-    WindowsPath foo = (WindowsPath) msRoot.getRelative("foo");
-    foo.applyToChildren(collector);
-    assertThat(children).containsExactly("D:/program files/microsoft something/foo/~bar_hello");
-
-    // Pretend that a path we already failed to resolve once came into existence.
-    shortPathResolver.resolutions.put(
-        "d:/program files/microsoft something/foo/will~1.exi", "will.exist");
-
-    // Assert that this time we can resolve the previously non-existent path.
-    shortPathResolver.resolutionQueries.clear();
-    Path nowExists = root.getRelative("d:/progra~1/micros~1/foo/will~1.exi/bar");
-    // The path string has an upper-case drive letter because that's how path printing works.
-    assertThat(nowExists.getPathString())
-        .isEqualTo("D:/program files/microsoft something/foo/will.exist/bar");
-    // Assert that we only try to resolve the path segments that look like they may be shortened.
-    assertThat(shortPathResolver.resolutionQueries)
-        .containsExactly(
-            "d:/progra~1",
-            "d:/program files/micros~1",
-            "d:/program files/microsoft something/foo/will~1.exi");
-
-    // Assert that this time we cached the previously non-existent path.
-    children.clear();
-    foo.applyToChildren(collector);
-    // The path strings have upper-case drive letters because that's how path printing works.
-    children.clear();
-    foo.applyToChildren(collector);
-    assertThat(children)
-        .containsExactly(
-            "D:/program files/microsoft something/foo/~bar_hello",
-            "D:/program files/microsoft something/foo/will.exist");
-  }
-
-  @Test
-  public void testCaseInsensitivePathFragment() {
-    // equals
-    assertThat(PathFragment.create("c:/FOO/BAR")).isEqualTo(PathFragment.create("c:\\foo\\bar"));
-    assertThat(PathFragment.create("c:/FOO/BAR")).isNotEqualTo(PathFragment.create("d:\\foo\\bar"));
-    assertThat(PathFragment.create("c:/FOO/BAR")).isNotEqualTo(PathFragment.create("/foo/bar"));
-    // equals for the string representation
-    assertThat(PathFragment.create("c:/FOO/BAR").toString())
-        .isNotEqualTo(PathFragment.create("c:/foo/bar").toString());
-    // hashCode
-    assertThat(PathFragment.create("c:/FOO/BAR").hashCode())
-        .isEqualTo(PathFragment.create("c:\\foo\\bar").hashCode());
-    assertThat(PathFragment.create("c:/FOO/BAR").hashCode())
-        .isNotEqualTo(PathFragment.create("d:\\foo\\bar").hashCode());
-    assertThat(PathFragment.create("c:/FOO/BAR").hashCode())
-        .isNotEqualTo(PathFragment.create("/foo/bar").hashCode());
-    // compareTo
-    assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("c:\\foo\\bar")))
-        .isEqualTo(0);
-    assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("d:\\foo\\bar")))
-        .isLessThan(0);
-    assertThat(PathFragment.create("c:/FOO/BAR").compareTo(PathFragment.create("/foo/bar")))
-        .isGreaterThan(0);
-    // startsWith
-    assertThat(PathFragment.create("c:/FOO/BAR").startsWith(PathFragment.create("c:\\foo")))
-        .isTrue();
-    assertThat(PathFragment.create("c:/FOO/BAR").startsWith(PathFragment.create("d:\\foo")))
-        .isFalse();
-    // endsWith
-    assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("bar\\baz")))
-        .isTrue();
-    assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("/bar/baz")))
-        .isFalse();
-    assertThat(PathFragment.create("c:/FOO/BAR/BAZ").endsWith(PathFragment.create("d:\\bar\\baz")))
-        .isFalse();
-    // relativeTo
-    assertThat(
-            PathFragment.create("c:/FOO/BAR/BAZ/QUX")
-                .relativeTo(PathFragment.create("c:\\foo\\bar")))
-        .isEqualTo(PathFragment.create("Baz/Qux"));
-  }
-
-  @Test
-  public void testCaseInsensitiveRootedPath() {
-    Path ancestor = filesystem.getPath("C:\\foo\\bar");
-    assertThat(ancestor).isInstanceOf(WindowsPath.class);
-    Path child = filesystem.getPath("C:\\FOO\\Bar\\baz");
-    assertThat(child).isInstanceOf(WindowsPath.class);
-    assertThat(child.startsWith(ancestor)).isTrue();
-    assertThat(child.relativeTo(ancestor)).isEqualTo(PathFragment.create("baz"));
-    RootedPath actual = RootedPath.toRootedPath(Root.fromPath(ancestor), child);
-    assertThat(actual.getRoot()).isEqualTo(Root.fromPath(ancestor));
-    assertThat(actual.getRootRelativePath()).isEqualTo(PathFragment.create("baz"));
-  }
-
-  @Test
-  public void testToURI() {
-    // See https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/
-    Path p = root.getRelative("Temp\\Foo Bar.txt");
-    URI uri = p.toURI();
-    assertThat(uri.toString()).isEqualTo("file:///C:/Temp/Foo%20Bar.txt");
-  }
-}
diff --git a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
index 8937c45..3e9cd27 100644
--- a/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
+++ b/src/test/java/com/google/devtools/build/lib/windows/WindowsFileSystemTest.java
@@ -18,7 +18,6 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
@@ -27,16 +26,13 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Symlinks;
-import com.google.devtools.build.lib.windows.WindowsFileSystem.WindowsPath;
 import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
 import com.google.devtools.build.lib.windows.util.WindowsTestUtil;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import org.junit.After;
 import org.junit.Before;
@@ -246,42 +242,22 @@
     assertThat(p.getPathString()).endsWith(longPath);
     assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(shortPath));
     assertThat(p).isEqualTo(fs.getPath(scratchRoot).getRelative(longPath));
-    assertThat(fs.getPath(scratchRoot).getRelative(shortPath)).isSameAs(p);
-    assertThat(fs.getPath(scratchRoot).getRelative(longPath)).isSameAs(p);
+    assertThat(fs.getPath(scratchRoot).getRelative(shortPath)).isEqualTo(p);
+    assertThat(fs.getPath(scratchRoot).getRelative(longPath)).isEqualTo(p);
   }
 
   @Test
   public void testUnresolvableShortPathWhichIsThenCreated() throws Exception {
     String shortPath = "unreso~1.sho/foo/will~1.exi/bar/hello.txt";
-    String longPrefix = "unresolvable.shortpath/foo/";
-    String longPath = longPrefix + "will.exist/bar/hello.txt";
-    testUtil.scratchDir(longPrefix);
-    final WindowsPath foo = (WindowsPath) fs.getPath(scratchRoot).getRelative(longPrefix);
-
+    String longPath = "unresolvable.shortpath/foo/will.exist/bar/hello.txt";
     // Assert that we can create an unresolvable path.
     Path p = fs.getPath(scratchRoot).getRelative(shortPath);
-    assertThat(p.getPathString()).endsWith(longPrefix + "will~1.exi/bar/hello.txt");
-    // Assert that said path is not cached in its parent's `children` cache.
-    final List<String> children = new ArrayList<>();
-    Predicate<Path> collector =
-        new Predicate<Path>() {
-          @Override
-          public boolean apply(Path child) {
-            children.add(child.relativeTo(foo).getPathString());
-            return true;
-          }
-        };
-    foo.applyToChildren(collector);
-    assertThat(children).isEmpty();
+    assertThat(p.getPathString()).endsWith(shortPath);
     // Assert that we can then create the whole path, and can now resolve the short form.
     testUtil.scratchFile(longPath, "hello");
     Path q = fs.getPath(scratchRoot).getRelative(shortPath);
     assertThat(q.getPathString()).endsWith(longPath);
-    // Assert however that the unresolved and resolved Path objects are different, and only the
-    // resolved one is cached.
     assertThat(p).isNotEqualTo(q);
-    foo.applyToChildren(collector);
-    assertThat(children).containsExactly("will.exist");
   }
 
   /**
@@ -297,18 +273,11 @@
     Path p2 = fs.getPath(scratchRoot).getRelative("longpa~1");
     assertThat(p1.exists()).isFalse();
     assertThat(p1).isEqualTo(p2);
-    assertThat(p1).isNotSameAs(p2);
 
     testUtil.scratchDir("longpathnow");
     Path q1 = fs.getPath(scratchRoot).getRelative("longpa~1");
-    Path q2 = fs.getPath(scratchRoot).getRelative("longpa~1");
     assertThat(q1.exists()).isTrue();
-    assertThat(q1).isEqualTo(q2);
-    // Assert q1 == q2, because we could successfully resolve the short path to a long name and we
-    // cache them by the long name, so it's irrelevant they were created from a 8dot3 name, or what
-    // that name resolves to later in time.
-    assertThat(q1).isSameAs(q2);
-    assertThat(q1).isSameAs(fs.getPath(scratchRoot).getRelative("longpathnow"));
+    assertThat(q1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpathnow"));
 
     // Delete the original resolution of "longpa~1" ("longpathnow").
     assertThat(q1.delete()).isTrue();
@@ -317,14 +286,8 @@
     // Create a directory whose 8dot3 name is also "longpa~1" but its long name is different.
     testUtil.scratchDir("longpaththen");
     Path r1 = fs.getPath(scratchRoot).getRelative("longpa~1");
-    Path r2 = fs.getPath(scratchRoot).getRelative("longpa~1");
     assertThat(r1.exists()).isTrue();
-    assertThat(r1).isEqualTo(r2);
-    assertThat(r1).isSameAs(r2);
-    assertThat(r1).isSameAs(fs.getPath(scratchRoot).getRelative("longpaththen"));
-    // r1 == r2 and q1 == q2, but r1 != q1, because the resolution of "longpa~1" changed over time.
-    assertThat(r1).isNotEqualTo(q1);
-    assertThat(r1).isNotSameAs(q1);
+    assertThat(r1).isEqualTo(fs.getPath(scratchRoot).getRelative("longpaththen"));
   }
 
   @Test