Introduce Fileset manifest RESOLVE_FULLY behavior, which resolves internal Fileset directory links

Instead of trying to further encode symlink resolution with full generality, we re-use existing logic to do so found in InMemoryFilesystem. Therefore we are able to encode the Fileset layout in the in-memory filesystem and let the existing logic do the resolution for us.

RELNOTES: None
PiperOrigin-RevId: 314959366
diff --git a/src/main/java/com/google/devtools/build/lib/actions/BUILD b/src/main/java/com/google/devtools/build/lib/actions/BUILD
index d6aff69..1e0f888 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/actions/BUILD
@@ -68,6 +68,7 @@
         "//src/main/java/com/google/devtools/build/lib/util/io",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
         "//src/main/java/com/google/devtools/build/skyframe",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//src/main/java/com/google/devtools/common/options",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FilesetManifest.java b/src/main/java/com/google/devtools/build/lib/actions/FilesetManifest.java
index f60f0ed..0dbdfdf 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/FilesetManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/FilesetManifest.java
@@ -16,7 +16,10 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.flogger.GoogleLogger;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
@@ -30,6 +33,7 @@
  * Representation of a Fileset manifest.
  */
 public final class FilesetManifest {
+  private static final int MAX_SYMLINK_TRAVERSALS = 256;
   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
 
   /**
@@ -43,7 +47,10 @@
     ERROR,
 
     /** Resolve all relative target paths. */
-    RESOLVE;
+    RESOLVE,
+
+    /** Fully resolve all relative paths, even those pointing to internal directories. */
+    RESOLVE_FULLY;
   }
 
   public static FilesetManifest constructFilesetManifest(
@@ -66,7 +73,7 @@
         artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
       }
     }
-    resolveRelativeSymlinks(entries, relativeLinks);
+    resolveRelativeSymlinks(entries, relativeLinks, targetPrefix.isAbsolute(), relSymlinkBehavior);
     return new FilesetManifest(entries, artifactValues);
   }
 
@@ -87,6 +94,7 @@
       case ERROR:
         throw new IOException("runfiles target is not absolute: " + artifact);
       case RESOLVE:
+      case RESOLVE_FULLY:
         if (!relativeLinks.containsKey(fullLocation)) { // Keep consistent behavior: no overwriting.
           relativeLinks.put(fullLocation, artifact);
         }
@@ -96,51 +104,106 @@
     }
   }
 
-  private static final int MAX_SYMLINK_TRAVERSALS = 256;
+  /** Fully resolve relative symlinks including internal directory symlinks. */
+  private static void fullyResolveRelativeSymlinks(
+      Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks, boolean absolute)
+      throws IOException {
+    // Construct an in-memory Filesystem containing all the non-relative-symlink entries in the
+    // Fileset. Treat these as regular files in the filesystem whose contents are the "real" symlink
+    // pointing out of the Fileset. For relative symlinks, we encode these as symlinks in the
+    // in-memory Filesystem. This allows us to then crawl the filesystem for files. Any readable
+    // file is a valid part of the FilesetManifest. Dangling internal links or symlink cycles will
+    // be discovered by the in-memory filesystem.
+    InMemoryFileSystem fs = new InMemoryFileSystem();
+    Path root = fs.getPath("/");
+    for (Map.Entry<PathFragment, String> e : entries.entrySet()) {
+      PathFragment location = e.getKey();
+      Path locationPath = root.getRelative(location);
+      locationPath.getParentDirectory().createDirectoryAndParents();
+      FileSystemUtils.writeContentAsLatin1(locationPath, Strings.nullToEmpty(e.getValue()));
+    }
+    for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
+      PathFragment location = e.getKey();
+      Path locationPath = fs.getPath("/").getRelative(location);
+      PathFragment value = PathFragment.create(Preconditions.checkNotNull(e.getValue(), e));
+      Preconditions.checkState(!value.isAbsolute(), e);
+
+      locationPath.getParentDirectory().createDirectoryAndParents();
+      locationPath.createSymbolicLink(value);
+    }
+
+    addSymlinks(root, entries, absolute);
+  }
+
+  private static void addSymlinks(Path root, Map<PathFragment, String> entries, boolean absolute)
+      throws IOException {
+    for (Path path : root.getDirectoryEntries()) {
+      try {
+        if (path.isDirectory()) {
+          addSymlinks(path, entries, absolute);
+        } else {
+          String contents = new String(FileSystemUtils.readContentAsLatin1(path));
+          entries.put(
+              absolute ? path.asFragment() : path.asFragment().toRelative(),
+              Strings.emptyToNull(contents));
+        }
+      } catch (IOException e) {
+        logger.atWarning().log("Symlink %s is dangling or cyclic: %s", path, e.getMessage());
+      }
+    }
+  }
 
   /**
    * Resolves relative symlinks and puts them in the {@code entries} map.
    *
    * <p>Note that {@code relativeLinks} should only contain entries in {@link
-   * RelativeSymlinkBehavior#RESOLVE} mode.
+   * RelativeSymlinkBehavior#RESOLVE} or {@link RelativeSymlinkBehavior#RESOLVE_FULLY} mode.
    */
   private static void resolveRelativeSymlinks(
-      Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks) {
-    for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
-      PathFragment location = e.getKey();
-      String value = e.getValue();
-      String actual = Preconditions.checkNotNull(value, e);
-      Preconditions.checkState(!actual.startsWith("/"), e);
-      PathFragment actualLocation = location;
+      Map<PathFragment, String> entries,
+      Map<PathFragment, String> relativeLinks,
+      boolean absolute,
+      RelativeSymlinkBehavior relSymlinkBehavior)
+      throws IOException {
+    if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE_FULLY && !relativeLinks.isEmpty()) {
+      fullyResolveRelativeSymlinks(entries, relativeLinks, absolute);
+    } else if (relSymlinkBehavior == RelativeSymlinkBehavior.RESOLVE) {
+      for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
+        PathFragment location = e.getKey();
+        String value = e.getValue();
+        String actual = Preconditions.checkNotNull(value, e);
+        Preconditions.checkState(!actual.startsWith("/"), e);
+        PathFragment actualLocation = location;
 
-      // Recursively resolve relative symlinks.
-      LinkedHashSet<String> seen = new LinkedHashSet<>();
-      int traversals = 0;
-      do {
-        actualLocation = actualLocation.getParentDirectory().getRelative(actual);
-        actual = relativeLinks.get(actualLocation);
-      } while (++traversals <= MAX_SYMLINK_TRAVERSALS && actual != null && seen.add(actual));
+        // Recursively resolve relative symlinks.
+        LinkedHashSet<String> seen = new LinkedHashSet<>();
+        int traversals = 0;
+        do {
+          actualLocation = actualLocation.getParentDirectory().getRelative(actual);
+          actual = relativeLinks.get(actualLocation);
+        } while (++traversals <= MAX_SYMLINK_TRAVERSALS && actual != null && seen.add(actual));
 
-      if (traversals >= MAX_SYMLINK_TRAVERSALS) {
-        logger.atWarning().log(
-            "Symlink %s is part of a chain of length at least %d"
-                + " which exceeds Blaze's maximum allowable symlink chain length",
-            location, traversals);
-      } else if (actual != null) {
-        // TODO(b/113128395): throw here.
-        logger.atWarning().log("Symlink %s forms a symlink cycle: %s", location, seen);
-      } else if (!entries.containsKey(actualLocation)) {
-        // We've found a relative symlink that points out of the fileset. We should really always
-        // throw here, but current behavior is that we tolerate such symlinks when they occur in
-        // runfiles, which is the only time this code is hit.
-        // TODO(b/113128395): throw here.
-        logger.atWarning().log(
-            "Symlink %s (transitively) points to %s"
-                + " that is not in this fileset (or was pruned because of a cycle)",
-            location, actualLocation);
-      } else {
-        // We have successfully resolved the symlink.
-        entries.put(location, entries.get(actualLocation));
+        if (traversals >= MAX_SYMLINK_TRAVERSALS) {
+          logger.atWarning().log(
+              "Symlink %s is part of a chain of length at least %d"
+                  + " which exceeds Blaze's maximum allowable symlink chain length",
+              location, traversals);
+        } else if (actual != null) {
+          // TODO(b/113128395): throw here.
+          logger.atWarning().log("Symlink %s forms a symlink cycle: %s", location, seen);
+        } else if (!entries.containsKey(actualLocation)) {
+          // We've found a relative symlink that points out of the fileset. We should really always
+          // throw here, but current behavior is that we tolerate such symlinks when they occur in
+          // runfiles, which is the only time this code is hit.
+          // TODO(b/113128395): throw here.
+          logger.atWarning().log(
+              "Symlink %s (transitively) points to %s"
+                  + " that is not in this fileset (or was pruned because of a cycle)",
+              location, actualLocation);
+        } else {
+          // We have successfully resolved the symlink.
+          entries.put(location, entries.get(actualLocation));
+        }
       }
     }
   }
diff --git a/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java b/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
index 8024bc1..b3d63ba 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
@@ -17,20 +17,33 @@
 import static com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehavior.ERROR;
 import static com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehavior.IGNORE;
 import static com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehavior.RESOLVE;
+import static com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehavior.RESOLVE_FULLY;
 import static org.junit.Assert.assertThrows;
 
+import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.actions.FilesetManifest;
+import com.google.devtools.build.lib.actions.FilesetManifest.RelativeSymlinkBehavior;
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.exec.FilesetManifestTest.ManifestCommonTests;
+import com.google.devtools.build.lib.exec.FilesetManifestTest.OneOffManifestTests;
+import com.google.devtools.build.lib.exec.FilesetManifestTest.ResolvingManifestTests;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Suite;
 
 /** Tests for {@link FilesetManifest}. */
-@RunWith(JUnit4.class)
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  ManifestCommonTests.class,
+  OneOffManifestTests.class,
+  ResolvingManifestTests.class
+})
 public final class FilesetManifestTest {
 
   private static final PathFragment EXEC_ROOT = PathFragment.create("/root");
@@ -40,176 +53,289 @@
         PathFragment.create(from), PathFragment.create(to), EXEC_ROOT);
   }
 
-  @Test
-  public void testEmptyManifest() throws Exception {
-    List<FilesetOutputSymlink> symlinks = ImmutableList.of();
+  /** Manifest tests that apply to all relative symlink behavior. */
+  @RunWith(Parameterized.class)
+  public static final class ManifestCommonTests {
+    private final RelativeSymlinkBehavior behavior;
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+    @Parameterized.Parameters
+    public static ImmutableCollection<Object[]> behaviors() {
+      return ImmutableList.of(
+          new Object[] {ERROR},
+          new Object[] {RESOLVE},
+          new Object[] {IGNORE},
+          new Object[] {RESOLVE_FULLY});
+    }
 
-    assertThat(manifest.getEntries()).isEmpty();
+    public ManifestCommonTests(RelativeSymlinkBehavior behavior) {
+      this.behavior = behavior;
+    }
+
+    @Test
+    public void testEmptyManifest() throws Exception {
+      List<FilesetOutputSymlink> symlinks = ImmutableList.of();
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries()).isEmpty();
+    }
+
+    @Test
+    public void testManifestWithSingleFile() throws Exception {
+      List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "/dir/file"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(PathFragment.create("out/foo/bar"), "/dir/file");
+    }
+
+    @Test
+    public void testManifestWithTwoFiles() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "/dir/file"), filesetSymlink("baz", "/dir/file"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(
+              PathFragment.create("out/foo/bar"), "/dir/file",
+              PathFragment.create("out/foo/baz"), "/dir/file");
+    }
+
+    @Test
+    public void testManifestWithDirectory() throws Exception {
+      List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "/some"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(PathFragment.create("out/foo/bar"), "/some");
+    }
+
+    /** Regression test: code was previously crashing in this case. */
+    @Test
+    public void testManifestWithEmptyPath() throws Exception {
+      List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", ""));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/foo/bar"), null);
+    }
+
+    @Test
+    public void testManifestWithExecRootRelativePath() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", EXEC_ROOT.getRelative("foo/bar").getPathString()));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(PathFragment.create("out/foo/bar"), "foo/bar");
+    }
+
+    /** Current behavior is first one wins. */
+    @Test
+    public void testDefactoBehaviorWithDuplicateEntries() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "/foo/bar"), filesetSymlink("bar", "/baz"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(PathFragment.create("out/foo/bar"), "/foo/bar");
+    }
   }
 
-  @Test
-  public void testManifestWithSingleFile() throws Exception {
-    List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "/dir/file"));
+  /** Manifest tests that apply to a specific relative symlink behavior. */
+  @RunWith(JUnit4.class)
+  public static final class OneOffManifestTests {
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+    @Test
+    public void testManifestWithErrorOnRelativeSymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
 
-    assertThat(manifest.getEntries())
-        .containsExactly(PathFragment.create("out/foo/bar"), "/dir/file");
+      IOException e =
+          assertThrows(
+              IOException.class,
+              () ->
+                  FilesetManifest.constructFilesetManifest(
+                      symlinks, PathFragment.create("out/foo"), ERROR));
+      assertThat(e).hasMessageThat().isEqualTo("runfiles target is not absolute: foo");
+    }
+
+    @Test
+    public void testManifestWithIgnoredRelativeSymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), IGNORE);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(PathFragment.create("out/foo/foo"), "/foo/bar");
+    }
+
+    @Test
+    public void testManifestWithResolvedRelativeDirectorySymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(
+              filesetSymlink("foo/subdir/f1", "/foo/subdir/f1"),
+              filesetSymlink("foo/bar", "subdir"));
+
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out"), RESOLVE_FULLY);
+
+      assertThat(manifest.getEntries())
+          .containsExactly(
+              PathFragment.create("out/foo/subdir/f1"), "/foo/subdir/f1",
+              PathFragment.create("out/foo/bar/f1"), "/foo/subdir/f1");
+    }
   }
 
-  @Test
-  public void testManifestWithTwoFiles() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "/dir/file"), filesetSymlink("baz", "/dir/file"));
+  /** Manifest tests that apply resolving relative symlink behavior. */
+  @RunWith(Parameterized.class)
+  public static final class ResolvingManifestTests {
+    private final RelativeSymlinkBehavior behavior;
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+    @Parameterized.Parameters
+    public static ImmutableCollection<Object[]> behaviors() {
+      return ImmutableList.of(new Object[] {RESOLVE}, new Object[] {RESOLVE_FULLY});
+    }
 
-    assertThat(manifest.getEntries())
-        .containsExactly(
-            PathFragment.create("out/foo/bar"), "/dir/file",
-            PathFragment.create("out/foo/baz"), "/dir/file");
-  }
+    public ResolvingManifestTests(RelativeSymlinkBehavior behavior) {
+      this.behavior = behavior;
+    }
 
-  @Test
-  public void testManifestWithDirectory() throws Exception {
-    List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "/some"));
+    @Test
+    public void testManifestWithResolvedRelativeSymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
 
-    assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/foo/bar"), "/some");
-  }
+      assertThat(manifest.getEntries())
+          .containsExactly(
+              PathFragment.create("out/foo/bar"), "/foo/bar",
+              PathFragment.create("out/foo/foo"), "/foo/bar");
+    }
 
-  /** Regression test: code was previously crashing in this case. */
-  @Test
-  public void testManifestWithEmptyPath() throws Exception {
-    List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", ""));
+    @Test
+    public void testManifestWithResolvedRelativeSymlinkWithDotSlash() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "./foo"), filesetSymlink("foo", "/foo/bar"));
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
 
-    assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/foo/bar"), null);
-  }
+      assertThat(manifest.getEntries())
+          .containsExactly(
+              PathFragment.create("out/foo/bar"), "/foo/bar",
+              PathFragment.create("out/foo/foo"), "/foo/bar");
+    }
 
-  @Test
-  public void testManifestWithErrorOnRelativeSymlink() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
+    @Test
+    public void testManifestWithSymlinkCycle() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(
+              filesetSymlink("bar", "foo"),
+              filesetSymlink("foo", "biz"),
+              filesetSymlink("biz", "bar"),
+              filesetSymlink("reg", "/file"));
 
-    IOException e =
-        assertThrows(
-            IOException.class,
-            () ->
-                FilesetManifest.constructFilesetManifest(
-                    symlinks, PathFragment.create("out/foo"), ERROR));
-    assertThat(e).hasMessageThat().isEqualTo("runfiles target is not absolute: foo");
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out"), behavior);
 
-  @Test
-  public void testManifestWithIgnoredRelativeSymlink() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
+      assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/reg"), "/file");
+    }
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
+    @Test
+    public void testUnboundedSymlinkDescendant() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(
+              filesetSymlink("p", "a/b"),
+              filesetSymlink("a/b", "../b/c"),
+              filesetSymlink("reg", "/file"));
 
-    assertThat(manifest.getEntries())
-        .containsExactly(PathFragment.create("out/foo/foo"), "/foo/bar");
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out"), behavior);
 
-  @Test
-  public void testManifestWithResolvedRelativeSymlink() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "/foo/bar"));
+      assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/reg"), "/file");
+    }
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), RESOLVE);
+    @Test
+    public void testUnboundedSymlinkAncestor() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(
+              filesetSymlink("a/b", "c/d"),
+              filesetSymlink("a/c/d", ".././a"),
+              filesetSymlink("reg", "/file"));
 
-    assertThat(manifest.getEntries())
-        .containsExactly(
-            PathFragment.create("out/foo/bar"), "/foo/bar",
-            PathFragment.create("out/foo/foo"), "/foo/bar");
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out"), behavior);
 
-  @Test
-  public void testManifestWithResolvedRelativeSymlinkWithDotSlash() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "./foo"), filesetSymlink("foo", "/foo/bar"));
+      assertThat(manifest.getEntries()).containsExactly(PathFragment.create("out/reg"), "/file");
+    }
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), RESOLVE);
+    @Test
+    public void testManifestWithResolvedRelativeSymlinkWithDotDotSlash() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(
+              filesetSymlink("bar/bar", "../foo/foo"), filesetSymlink("foo/foo", "/foo/bar"));
 
-    assertThat(manifest.getEntries())
-        .containsExactly(
-            PathFragment.create("out/foo/bar"), "/foo/bar",
-            PathFragment.create("out/foo/foo"), "/foo/bar");
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
 
-  @Test
-  public void testManifestWithResolvedRelativeSymlinkWithDotDotSlash() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(
-            filesetSymlink("bar/bar", "../foo/foo"), filesetSymlink("foo/foo", "/foo/bar"));
+      assertThat(manifest.getEntries())
+          .containsExactly(
+              PathFragment.create("out/foo/bar/bar"), "/foo/bar",
+              PathFragment.create("out/foo/foo/foo"), "/foo/bar");
+    }
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), RESOLVE);
+    @Test
+    public void testManifestWithUnresolvableRelativeSymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "foo"));
 
-    assertThat(manifest.getEntries())
-        .containsExactly(
-            PathFragment.create("out/foo/bar/bar"), "/foo/bar",
-            PathFragment.create("out/foo/foo/foo"), "/foo/bar");
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
 
-  @Test
-  public void testManifestWithUnresolvableRelativeSymlink() throws Exception {
-    List<FilesetOutputSymlink> symlinks = ImmutableList.of(filesetSymlink("bar", "foo"));
+      assertThat(manifest.getEntries()).isEmpty();
+      assertThat(manifest.getArtifactValues()).isEmpty();
+    }
 
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), RESOLVE);
+    @Test
+    public void testManifestWithUnresolvableRelativeSymlinkToRelativeSymlink() throws Exception {
+      List<FilesetOutputSymlink> symlinks =
+          ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "baz"));
 
-    assertThat(manifest.getEntries()).isEmpty();
-    assertThat(manifest.getArtifactValues()).isEmpty();
-  }
+      FilesetManifest manifest =
+          FilesetManifest.constructFilesetManifest(
+              symlinks, PathFragment.create("out/foo"), behavior);
 
-  @Test
-  public void testManifestWithUnresolvableRelativeSymlinkToRelativeSymlink() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "foo"), filesetSymlink("foo", "baz"));
-
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), RESOLVE);
-
-    assertThat(manifest.getEntries()).isEmpty();
-    assertThat(manifest.getArtifactValues()).isEmpty();
-  }
-
-  /** Current behavior is first one wins. */
-  @Test
-  public void testDefactoBehaviorWithDuplicateEntries() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", "/foo/bar"), filesetSymlink("bar", "/baz"));
-
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
-
-    assertThat(manifest.getEntries())
-        .containsExactly(PathFragment.create("out/foo/bar"), "/foo/bar");
-  }
-
-  @Test
-  public void testManifestWithExecRootRelativePath() throws Exception {
-    List<FilesetOutputSymlink> symlinks =
-        ImmutableList.of(filesetSymlink("bar", EXEC_ROOT.getRelative("foo/bar").getPathString()));
-
-    FilesetManifest manifest =
-        FilesetManifest.constructFilesetManifest(symlinks, PathFragment.create("out/foo"), IGNORE);
-
-    assertThat(manifest.getEntries())
-        .containsExactly(PathFragment.create("out/foo/bar"), "foo/bar");
+      assertThat(manifest.getEntries()).isEmpty();
+      assertThat(manifest.getArtifactValues()).isEmpty();
+    }
   }
 }