Make UnionFileSystem accept all paths Bazel can throw at it.

Instead of relying on a character-by-character StringTrie, segment paths based on PathFragments. This means UnionFS can accept any path that Bazel stores internally, removing the ASCII limitations.

This also means removing the ability to have a filesystem bound for sub-PathFragments, /foo/barbar, /foo/barqux could have the same filesystem bound at /foo/bar. This feature was tested for when a use case was envisioned, but it was never used, so removing it is safe.

RELNOTES: None.
PiperOrigin-RevId: 170054656
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 d1d82d9..de65cf7 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
@@ -17,8 +17,6 @@
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.concurrent.ThreadSafety;
 import com.google.devtools.build.lib.util.Preconditions;
-import com.google.devtools.build.lib.util.StringTrie;
-import com.google.devtools.build.lib.vfs.FileSystem.HashFunction;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -28,21 +26,20 @@
 
 /**
  * Presents a unified view of multiple virtual {@link FileSystem} instances, to which requests are
- * delegated based on a {@link PathFragment} prefix mapping.
- * If multiple prefixes apply to a given path, the *longest* (i.e. most specific) match is used.
- * The order in which the delegates are specified does not influence the mapping.
+ * delegated based on a {@link PathFragment} prefix mapping. If multiple prefixes apply to a given
+ * path, the *longest* (i.e. most specific) match is used. The order in which the delegates are
+ * specified does not influence the mapping.
  *
- * <p>Paths are preserved absolutely, contrary to how "mount" works, e.g.:
- *    /foo/bar maps to /foo/bar on the delegate, even if it is mounted at /foo.
+ * <p>Paths are preserved absolutely, contrary to how "mount" works, e.g.: /foo/bar maps to /foo/bar
+ * on the delegate, even if it is mounted at /foo.
  *
- * <p>For example:
- * "/in" maps to InFileSystem, "/" maps to OtherFileSystem.
- * Reading from "/in/base/BUILD" through the UnionFileSystem will delegate the read operation to
- * InFileSystem, which will read "/in/base/BUILD" relative to its root.
- * ("mount" behavior would remap it to "/base/BUILD" on the delegate).
+ * <p>For example: "/in" maps to InFileSystem, "/" maps to OtherFileSystem. Reading from
+ * "/in/base/BUILD" through the UnionFileSystem will delegate the read operation to InFileSystem,
+ * which will read "/in/base/BUILD" relative to its root. ("mount" behavior would remap it to
+ * "/base/BUILD" on the delegate).
  *
- * <p>Intra-filesystem symbolic links are resolved to their ultimate targets.
- * Cross-filesystem links are not currently supported.
+ * <p>Intra-filesystem symbolic links are resolved to their ultimate targets. Cross-filesystem links
+ * are not currently supported.
  */
 @ThreadSafety.ThreadSafe
 public class UnionFileSystem extends FileSystem {
@@ -50,7 +47,7 @@
   // Prefix trie index, allowing children to easily inherit prefix mappings
   // of their parents.
   // This does not currently handle unicode filenames.
-  private StringTrie<FileSystem> pathDelegate;
+  private final PathTrie<FileSystem> pathDelegate;
 
   // True iff the filesystem can be modified. If false, mutating operations
   // will throw UnsupportedOperationExceptions.
@@ -61,37 +58,35 @@
   private final boolean isCaseSensitive;
 
   /**
-   * Creates a new modifiable UnionFileSystem with prefix mappings
-   * specified by a map.
+   * Creates a new modifiable UnionFileSystem with prefix mappings specified by a map.
    *
    * @param prefixMapping map of path prefixes to {@link FileSystem}s
    */
-  public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
-                         FileSystem rootFileSystem) {
+  public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem) {
     this(prefixMapping, rootFileSystem, /* readOnly */ false);
   }
 
   /**
-   * Creates a new modifiable or read-only UnionFileSystem with prefix mappings
-   * specified by a map.
+   * Creates a new modifiable or read-only UnionFileSystem with prefix mappings specified by a map.
    *
-   * @param prefixMapping map of path prefixes to delegate {@link FileSystem}s
+   * @param prefixMapping map of path prefixes to delegate {@link FileSystem} instances to use for
+   *     paths of that prefix. Note that all prefixes must be absolute paths.
    * @param rootFileSystem root for default requests; i.e. mapping of "/"
    * @param readOnly if true, mutating operations will throw
    */
-  public UnionFileSystem(Map<PathFragment, FileSystem> prefixMapping,
-                         FileSystem rootFileSystem, boolean readOnly) {
+  public UnionFileSystem(
+      Map<PathFragment, FileSystem> prefixMapping, FileSystem rootFileSystem, boolean readOnly) {
     super();
     Preconditions.checkNotNull(prefixMapping);
     Preconditions.checkNotNull(rootFileSystem);
     Preconditions.checkArgument(rootFileSystem != this, "Circular root filesystem.");
     Preconditions.checkArgument(
         !prefixMapping.containsKey(PathFragment.EMPTY_FRAGMENT),
-        "Attempted to specify an explicit root prefix mapping; " +
-        "please use the rootFileSystem argument instead.");
+        "Attempted to specify an explicit root prefix mapping; "
+            + "please use the rootFileSystem argument instead.");
 
     this.readOnly = readOnly;
-    this.pathDelegate = new StringTrie<>();
+    this.pathDelegate = new PathTrie<>();
     this.isCaseSensitive = rootFileSystem.isFilePathCaseSensitive();
 
     for (Map.Entry<PathFragment, FileSystem> prefix : prefixMapping.entrySet()) {
@@ -102,29 +97,24 @@
       PathFragment prefixPath = prefix.getKey();
 
       // Extra slash prevents within-directory mappings, which Path can't handle.
-      String path = prefixPath.getPathString();
-      pathDelegate.put(path, delegate);
+      pathDelegate.put(prefixPath, delegate);
     }
-    pathDelegate.put(PathFragment.EMPTY_FRAGMENT.getPathString(), rootFileSystem);
+    pathDelegate.put(PathFragment.ROOT_FRAGMENT, rootFileSystem);
   }
 
   /**
-   * Retrieves the filesystem delegate of a path mapping.
-   * Does not follow symlinks (but you can call on a path preprocessed with
-   * {@link #resolveSymbolicLinks} to support this use case).
+   * Retrieves the filesystem delegate of a path mapping. Does not follow symlinks (but you can call
+   * on a path preprocessed with {@link #resolveSymbolicLinks} to support this use case).
    *
    * @param path the {@link Path} to map to a filesystem
    * @throws IllegalArgumentException if no delegate exists for the path
    */
   protected FileSystem getDelegate(Path path) {
     Preconditions.checkNotNull(path);
-
-    String pathString = path.getPathString();
-    FileSystem immediateDelegate = pathDelegate.get(pathString);
+    FileSystem immediateDelegate = pathDelegate.get(path.asFragment());
 
     // Should never actually happen if the root delegate is present.
-    Preconditions.checkArgument(immediateDelegate != null, "No delegate filesystem exists for %s",
-        pathString);
+    Preconditions.checkNotNull(immediateDelegate, "No delegate filesystem exists for %s", path);
     return immediateDelegate;
   }
 
@@ -135,8 +125,8 @@
   }
 
   /**
-   * Follow a symbolic link once using the appropriate delegate filesystem, also
-   * resolving parent directory symlinks.
+   * Follow a symbolic link once using the appropriate delegate filesystem, also resolving parent
+   * directory symlinks.
    *
    * @param path {@link Path} to the symbolic link
    */
@@ -157,7 +147,7 @@
   private void checkModifiable() {
     if (!supportsModifications()) {
       throw new UnsupportedOperationException(
-          "Modifications to this " + getClass().getSimpleName() + " are disabled.");
+          String.format("Modifications to this %s are disabled.", getClass().getSimpleName()));
     }
   }
 
@@ -311,9 +301,8 @@
   }
 
   /**
-   * Retrieves the directory entries for the specified path under the assumption
-   * that {@code resolvedPath} is the resolved path of {@code path} in one of the
-   * underlying file systems.
+   * Retrieves the directory entries for the specified path under the assumption that {@code
+   * resolvedPath} is the resolved path of {@code path} in one of the underlying file systems.
    *
    * @param path the {@link Path} whose children are to be retrieved
    */
@@ -409,16 +398,18 @@
     FileSystem sourceDelegate = getDelegate(sourcePath);
     if (!sourceDelegate.supportsModifications()) {
       throw new UnsupportedOperationException(
-          "The filesystem for the source path "
-          + sourcePath.getPathString() + " does not support modifications.");
+          String.format(
+              "The filesystem for the source path %s does not support modifications.",
+              sourcePath.getPathString()));
     }
     sourcePath = adjustPath(sourcePath, sourceDelegate);
 
     FileSystem targetDelegate = getDelegate(targetPath);
     if (!targetDelegate.supportsModifications()) {
       throw new UnsupportedOperationException(
-          "The filesystem for the target path "
-          + targetPath.getPathString() + " does not support modifications.");
+          String.format(
+              "The filesystem for the target path %s does not support modifications.",
+              targetPath.getPathString()));
     }
     targetPath = adjustPath(targetPath, targetDelegate);
 
@@ -435,8 +426,7 @@
   }
 
   @Override
-  protected void createFSDependentHardLink(Path linkPath, Path originalPath)
-      throws IOException {
+  protected void createFSDependentHardLink(Path linkPath, Path originalPath) throws IOException {
     checkModifiable();
 
     FileSystem originalDelegate = getDelegate(originalPath);