// 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 java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.util.Preconditions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;

/**
 * Generic tests for any file system that implements {@link ScopeEscapableFileSystem},
 * i.e. any file system that supports symlinks that escape its scope.
 *
 * Each suitable file system test should inherit from this class, thereby obtaining
 * all the tests.
 */
public abstract class ScopeEscapableFileSystemTest extends SymlinkAwareFileSystemTest {

  /**
   * Trivial FileSystem implementation that can record the last path passed to each method
   * and read/write to a unified "state" variable (which can then be checked by tests) for
   * each data type this class manipulates.
   *
   * The default implementation of each method throws an exception. Each test case should
   * selectively override the methods it expects to be invoked.
   */
  private static class TestDelegator extends FileSystem {
    protected Path lastPath;
    protected boolean booleanState;
    protected long longState;
    protected Object objectState;

    public void setState(boolean state) { booleanState = state; }
    public void setState(long state) { longState = state; }
    public void setState(Object state) { objectState = state; }

    public boolean booleanState() { return booleanState; }
    public long longState() { return longState; }
    public Object objectState() { return objectState; }

    public PathFragment lastPath() {
      Path ans = lastPath;
      // Clear this out to protect against accidental matches when testing the same path multiple
      // consecutive times.
      lastPath = null;
      return ans != null ? ans.asFragment() : null;
    }

    @Override public boolean supportsModifications() {
      return true;
    }

    @Override public boolean supportsSymbolicLinksNatively() {
      return true;
    }

    @Override public boolean supportsHardLinksNatively() {
      return true;
    }

    @Override
    public boolean isFilePathCaseSensitive() {
      return true;
    }

    private static RuntimeException re() {
      return new RuntimeException("This method should not be called in this context");
    }

    @Override protected boolean isReadable(Path path) { throw re(); }
    @Override protected boolean isWritable(Path path) { throw re(); }
    @Override protected boolean isDirectory(Path path, boolean followSymlinks) { throw re(); }
    @Override protected boolean isFile(Path path, boolean followSymlinks) { throw re(); }
    @Override protected boolean isSpecialFile(Path path, boolean followSymlinks) { throw re(); }
    @Override protected boolean isExecutable(Path path) { throw re(); }
    @Override protected boolean exists(Path path, boolean followSymlinks) {throw re(); }
    @Override protected boolean isSymbolicLink(Path path) { throw re(); }
    @Override protected boolean createDirectory(Path path) { throw re(); }
    @Override protected boolean delete(Path path) { throw re(); }

    @Override protected long getFileSize(Path path, boolean followSymlinks) { throw re(); }
    @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { throw re(); }

    @Override protected void setWritable(Path path, boolean writable) { throw re(); }
    @Override protected void setExecutable(Path path, boolean executable) { throw re(); }
    @Override protected void setReadable(Path path, boolean readable) { throw re(); }
    @Override protected void setLastModifiedTime(Path path, long newTime) { throw re(); }
    @Override protected void renameTo(Path sourcePath, Path targetPath) { throw re(); }
    @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) {
      throw re();
    }
    @Override protected void createFSDependentHardLink(Path linkPath, Path originalPath) {
      throw re();
    }
    @Override protected PathFragment readSymbolicLink(Path path) { throw re(); }
    @Override protected InputStream getInputStream(Path path) { throw re(); }
    @Override protected Collection<Path> getDirectoryEntries(Path path) { throw re(); }
    @Override protected OutputStream getOutputStream(Path path, boolean append)  { throw re(); }
    @Override
    protected FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
      throw re();
    }
  }

  protected static final PathFragment SCOPE_ROOT = new PathFragment("/fs/root");

  private Path fileLink;
  private PathFragment fileLinkTarget;
  private Path dirLink;
  private PathFragment dirLinkTarget;

  @Before
  public final void createLinks() throws Exception  {
    Preconditions.checkState(
        testFS instanceof ScopeEscapableFileSystem, "Not ScopeEscapable: %s", testFS);
    ((ScopeEscapableFileSystem) testFS).enableScopeChecking(false);
    for (int i = 1; i <= SCOPE_ROOT.segmentCount(); i++) {
      testFS.getPath(SCOPE_ROOT.subFragment(0, i)).createDirectory();
    }

    fileLink = testFS.getPath(SCOPE_ROOT.getRelative("link"));
    fileLinkTarget = new PathFragment("/should/be/delegated/fileLinkTarget");
    testFS.createSymbolicLink(fileLink, fileLinkTarget);

    dirLink = testFS.getPath(SCOPE_ROOT.getRelative("dirlink"));
    dirLinkTarget = new PathFragment("/should/be/delegated/dirLinkTarget");
    testFS.createSymbolicLink(dirLink, dirLinkTarget);
  }

  /**
   * Returns the file system supplied by {@link #getFreshFileSystem}, cast to
   * a {@link ScopeEscapableFileSystem}. Also enables scope checking within
   * the file system (which we keep disabled for inherited tests that aren't
   * intended to test scope boundaries).
   */
  private ScopeEscapableFileSystem scopedFS() {
    ScopeEscapableFileSystem fs = (ScopeEscapableFileSystem) testFS;
    fs.enableScopeChecking(true);
    return fs;
  }

  // Checks that the semi-resolved path passed to the delegator matches the expected value.
  private void checkPath(TestDelegator delegator, PathFragment expectedDelegatedPath) {
    assertEquals(delegator.lastPath(), expectedDelegatedPath);
  }

  // Asserts that the condition is false and checks that the expected path was delegated.
  private void assertFalseWithPathCheck(boolean result, TestDelegator delegator,
      PathFragment expectedDelegatedPath) {
    assertFalse(result);
    checkPath(delegator, expectedDelegatedPath);
  }

  // Asserts that the condition is true and checks that the expected path was delegated.
  private void assertTrueWithPathCheck(boolean result, TestDelegator delegator,
      PathFragment expectedDelegatedPath) {
    assertTrue(result);
    checkPath(delegator, expectedDelegatedPath);
  }

  /////////////////////////////////////////////////////////////////////////////
  // Tests:
  /////////////////////////////////////////////////////////////////////////////

  @Test
  public void testIsReadableCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isReadable(Path path) {
        lastPath = path;
        return booleanState();
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").isReadable(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.isReadable(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").isReadable(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testIsWritableCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isWritable(Path path) {
        lastPath = path;
        return booleanState();
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").isWritable(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.isWritable(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").isWritable(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testisExecutableCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isExecutable(Path path) {
        lastPath = path;
        return booleanState();
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.isExecutable(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").isExecutable(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testIsDirectoryCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) {
        lastPath = path;
        return booleanState();
      }
      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.isDirectory(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").isDirectory(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testIsFileCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isFile(Path path, boolean followSymlinks) {
        lastPath = path;
        return booleanState();
      }
      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").isFile(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.isFile(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").isFile(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testIsSymbolicLinkCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean isSymbolicLink(Path path) {
        lastPath = path;
        return booleanState();
      }
      @Override protected boolean exists(Path path, boolean followSymlinks) { return true; }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    // We shouldn't follow final-segment links, so they should never invoke the delegator.
    delegator.setState(false);
    assertTrue(fileLink.isSymbolicLink());
    assertNull(delegator.lastPath());

    assertFalseWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(dirLink.getRelative("a").isSymbolicLink(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  /**
   * Returns a test delegator that reflects info passed to Path.exists() calls.
   */
  private TestDelegator newExistsDelegator() {
    return new TestDelegator() {
      @Override protected boolean exists(Path path, boolean followSymlinks) {
        lastPath = path;
        return booleanState();
      }
      @Override protected FileStatus stat(Path path, boolean followSymlinks) throws IOException {
        if (!exists(path, followSymlinks)) {
          throw new IOException("Expected exception on stat of non-existent file");
        }
        return super.stat(path, followSymlinks);
      }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
    };
  }

  @Test
  public void testExistsCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = newExistsDelegator();
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(fileLink.exists(), delegator, fileLinkTarget);
    assertFalseWithPathCheck(dirLink.getRelative("a").exists(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(fileLink.exists(), delegator, fileLinkTarget);
    assertTrueWithPathCheck(dirLink.getRelative("a").exists(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCreateDirectoryCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean createDirectory(Path path) {
        lastPath = path;
        return booleanState();
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertFalseWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(dirLink.getRelative("a").createDirectory(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testDeleteCallOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected boolean delete(Path path) {
        lastPath = path;
        return booleanState();
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    assertTrue(fileLink.delete());
    assertNull(delegator.lastPath()); // Deleting a link shouldn't require delegation.
    assertFalseWithPathCheck(dirLink.getRelative("a").delete(), delegator,
        dirLinkTarget.getRelative("a"));

    delegator.setState(true);
    assertTrueWithPathCheck(dirLink.getRelative("a").delete(), delegator,
        dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallGetFileSizeOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected long getFileSize(Path path, boolean followSymlinks) {
        lastPath = path;
        return longState();
      }
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) { return 0; }
    };
    scopedFS().setDelegator(delegator);

    final int state1 = 10;
    delegator.setState(state1);
    assertEquals(state1, fileLink.getFileSize());
    checkPath(delegator, fileLinkTarget);
    assertEquals(state1, dirLink.getRelative("a").getFileSize());
    checkPath(delegator, dirLinkTarget.getRelative("a"));

    final int state2 = 10;
    delegator.setState(state2);
    assertEquals(state2, fileLink.getFileSize());
    checkPath(delegator, fileLinkTarget);
    assertEquals(state2, dirLink.getRelative("a").getFileSize());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
   }

  @Test
  public void testCallGetLastModifiedTimeOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected long getLastModifiedTime(Path path, boolean followSymlinks) {
        lastPath = path;
        return longState();
      }
    };
    scopedFS().setDelegator(delegator);

    final int state1 = 10;
    delegator.setState(state1);
    assertEquals(state1, fileLink.getLastModifiedTime());
    checkPath(delegator, fileLinkTarget);
    assertEquals(state1, dirLink.getRelative("a").getLastModifiedTime());
    checkPath(delegator, dirLinkTarget.getRelative("a"));

    final int state2 = 10;
    delegator.setState(state2);
    assertEquals(state2, fileLink.getLastModifiedTime());
    checkPath(delegator, fileLinkTarget);
    assertEquals(state2, dirLink.getRelative("a").getLastModifiedTime());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallSetReadableOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void setReadable(Path path, boolean readable) {
        lastPath = path;
        setState(readable);
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    fileLink.setReadable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);
    fileLink.setReadable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);

    delegator.setState(false);
    dirLink.getRelative("a").setReadable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
    dirLink.getRelative("a").setReadable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallSetWritableOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void setWritable(Path path, boolean writable) {
        lastPath = path;
        setState(writable);
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    fileLink.setWritable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);
    fileLink.setWritable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);

    delegator.setState(false);
    dirLink.getRelative("a").setWritable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
    dirLink.getRelative("a").setWritable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallSetExecutableOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void setReadable(Path path, boolean readable) {
        lastPath = path;
        setState(readable);
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(false);
    fileLink.setReadable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);
    fileLink.setReadable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, fileLinkTarget);

    delegator.setState(false);
    dirLink.getRelative("a").setReadable(true);
    assertTrue(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
    dirLink.getRelative("a").setReadable(false);
    assertFalse(delegator.booleanState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallSetLastModifiedTimeOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void setLastModifiedTime(Path path, long newTime) {
        lastPath = path;
        setState(newTime);
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(0);
    fileLink.setLastModifiedTime(10);
    assertEquals(10, delegator.longState());
    checkPath(delegator, fileLinkTarget);
    fileLink.setLastModifiedTime(15);
    assertEquals(15, delegator.longState());
    checkPath(delegator, fileLinkTarget);

    dirLink.getRelative("a").setLastModifiedTime(20);
    assertEquals(20, delegator.longState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
    dirLink.getRelative("a").setLastModifiedTime(25);
    assertEquals(25, delegator.longState());
    checkPath(delegator, dirLinkTarget.getRelative("a"));
  }

  @Test
  public void testCallRenameToOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void renameTo(Path sourcePath, Path targetPath) {
        lastPath = sourcePath;
        setState(targetPath);
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    // Renaming a link should work fine.
    delegator.setState(null);
    fileLink.renameTo(testFS.getPath(SCOPE_ROOT).getRelative("newname"));
    assertNull(delegator.lastPath()); // Renaming a link shouldn't require delegation.
    assertNull(delegator.objectState());

    // Renaming an out-of-scope path to an in-scope path should fail due to filesystem mismatch
    // errors.
    Path newPath = testFS.getPath(SCOPE_ROOT.getRelative("blah"));
    try {
      dirLink.getRelative("a").renameTo(newPath);
      fail("This is an attempt at a cross-filesystem renaming, which should fail");
    } catch (IOException e) {
      // Expected.
    }

    // Renaming an out-of-scope path to another out-of-scope path can be valid.
    newPath = dirLink.getRelative("b");
    dirLink.getRelative("a").renameTo(newPath);
    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
    assertEquals(dirLinkTarget.getRelative("b"), ((Path) delegator.objectState()).asFragment());
  }

  @Test
  public void testCallCreateSymbolicLinkOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected void createSymbolicLink(Path linkPath, PathFragment targetFragment) {
        lastPath = linkPath;
        setState(targetFragment);
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    PathFragment newLinkTarget = new PathFragment("/something/else");
    dirLink.getRelative("a").createSymbolicLink(newLinkTarget);
    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
    assertSame(newLinkTarget, delegator.objectState());
  }

  @Test
  public void testCallReadSymbolicLinkOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected PathFragment readSymbolicLink(Path path) {
        lastPath = path;
        return (PathFragment) objectState;
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    // Since we're not following the link, this shouldn't invoke delegation.
    delegator.setState(new PathFragment("whatever"));
    PathFragment p = fileLink.readSymbolicLink();
    assertNull(delegator.lastPath());
    assertNotSame(delegator.objectState(), p);

    // This should.
    p = dirLink.getRelative("a").readSymbolicLink();
    assertEquals(dirLinkTarget.getRelative("a"), delegator.lastPath());
    assertSame(delegator.objectState(), p);
  }

  @Test
  public void testCallGetInputStreamOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected InputStream getInputStream(Path path) {
        lastPath = path;
        return (InputStream) objectState;
      }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(new ByteArrayInputStream("blah".getBytes(UTF_8)));
    InputStream is = fileLink.getInputStream();
    assertEquals(fileLinkTarget, delegator.lastPath());
    assertSame(delegator.objectState(), is);

    delegator.setState(new ByteArrayInputStream("blah2".getBytes(UTF_8)));
    is = dirLink.getInputStream();
    assertEquals(dirLinkTarget, delegator.lastPath());
    assertSame(delegator.objectState(), is);
  }

  @Test
  public void testCallGetOutputStreamOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected OutputStream getOutputStream(Path path, boolean append)  {
        lastPath = path;
        return (OutputStream) objectState;
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(new ByteArrayOutputStream());
    OutputStream os = fileLink.getOutputStream();
    assertEquals(fileLinkTarget, delegator.lastPath());
    assertSame(delegator.objectState(), os);

    delegator.setState(new ByteArrayOutputStream());
    os = dirLink.getOutputStream();
    assertEquals(dirLinkTarget, delegator.lastPath());
    assertSame(delegator.objectState(), os);
  }

  @Test
  public void testCallGetDirectoryEntriesOnEscapingSymlink() throws Exception {
    TestDelegator delegator = new TestDelegator() {
      @Override protected Collection<Path> getDirectoryEntries(Path path) {
        lastPath = path;
        return ImmutableList.of((Path) objectState);
      }
      @Override protected boolean isDirectory(Path path, boolean followSymlinks) { return true; }
    };
    scopedFS().setDelegator(delegator);

    delegator.setState(testFS.getPath("/anything"));
    Collection<Path> entries = dirLink.getDirectoryEntries();
    assertEquals(dirLinkTarget, delegator.lastPath());
    assertThat(entries).hasSize(1);
    assertSame(delegator.objectState(), entries.iterator().next());
  }

  /**
   * Asserts that "link" is an in-scope link that doesn't result in an out-of-FS
   * delegation. If link is relative, its path is relative to SCOPE_ROOT.
   *
   * Note that we don't actually check that the canonicalized target path matches
   * the link's target value. Such testing should be covered by
   * SymlinkAwareFileSystemTest.
   */
  private void assertInScopeLink(String link, String target, TestDelegator d) throws IOException {
    Path l = testFS.getPath(SCOPE_ROOT.getRelative(link));
    testFS.createSymbolicLink(l, new PathFragment(target));
    l.exists();
    assertNull(d.lastPath());
  }

  /**
   * Asserts that "link" is an out-of-scope link and that the re-delegated path
   * matches expectedPath. If link is relative, its path is relative to SCOPE_ROOT.
   */
  private void assertOutOfScopeLink(String link, String target, String expectedPath,
      TestDelegator d) throws IOException {
    Path l = testFS.getPath(SCOPE_ROOT.getRelative(link));
    testFS.createSymbolicLink(l, new PathFragment(target));
    l.exists();
    assertEquals(expectedPath, d.lastPath().getPathString());
  }

  /**
   * Returns the scope root with the final n segments chopped off (or a 0-segment path
   * if n > SCOPE_ROOT.segmentCount()).
   */
  private String chopScopeRoot(int n) {
    return SCOPE_ROOT
        .subFragment(0, n > SCOPE_ROOT.segmentCount() ? 0 : SCOPE_ROOT.segmentCount() - n)
        .getPathString();
  }

  /**
   * Tests that absolute symlinks with ".." and "." segments are delegated to
   * the expected paths.
   */
  @Test
  public void testAbsoluteSymlinksWithParentReferences() throws Exception {
    TestDelegator d = newExistsDelegator();
    scopedFS().setDelegator(d);
    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir")));
    String scopeRoot = SCOPE_ROOT.getPathString();
    String scopeBase = SCOPE_ROOT.getBaseName();

    // Symlinks that should never escape our scope.
    assertInScopeLink("ilink1", scopeRoot, d);
    assertInScopeLink("ilink2", scopeRoot + "/target", d);
    assertInScopeLink("ilink3", scopeRoot + "/dir/../target", d);
    assertInScopeLink("ilink4", scopeRoot + "/dir/../dir/dir2/../target", d);
    assertInScopeLink("ilink5", scopeRoot + "/./dir/.././target", d);
    assertInScopeLink("ilink6", scopeRoot + "/../" + scopeBase + "/target", d);
    assertInScopeLink("ilink7", "/some/path/../.." + scopeRoot + "/target", d);

    // Symlinks that should escape our scope.
    assertOutOfScopeLink("olink1", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("olink2", "/some/other/path", "/some/other/path", d);
    assertOutOfScopeLink("olink3", scopeRoot + "/../target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("olink4", chopScopeRoot(1) + "/target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("olink5", scopeRoot + "/../../../../target", "/target", d);

    // In-scope symlink that's not the final segment in a query.
    Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("ilinkdir"));
    testFS.createSymbolicLink(iDirLink, SCOPE_ROOT.getRelative("dir"));
    iDirLink.getRelative("file").exists();
    assertNull(d.lastPath());

    // Out-of-scope symlink that's not the final segment in a query.
    Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("olinkdir"));
    testFS.createSymbolicLink(oDirLink, new PathFragment("/some/other/dir"));
    oDirLink.getRelative("file").exists();
    assertEquals("/some/other/dir/file", d.lastPath().getPathString());
  }

  /**
   * Tests that relative symlinks with ".." and "." segments are delegated to
   * the expected paths.
   */
  @Test
  public void testRelativeSymlinksWithParentReferences() throws Exception {
    TestDelegator d = newExistsDelegator();
    scopedFS().setDelegator(d);
    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir")));
    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2")));
    testFS.createDirectory(testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/dir3")));
    String scopeRoot = SCOPE_ROOT.getPathString();
    String scopeBase = SCOPE_ROOT.getBaseName();

    // Symlinks that should never escape our scope.
    assertInScopeLink("ilink1", "target", d);
    assertInScopeLink("ilink2", "dir/../otherdir/target", d);
    assertInScopeLink("dir/ilink3", "../target", d);
    assertInScopeLink("dir/dir2/ilink4", "../../target", d);
    assertInScopeLink("dir/dir2/ilink5", ".././../dir/./target", d);
    assertInScopeLink("dir/dir2/ilink6", "../dir2/../../dir/dir2/dir3/../../../target", d);

    // Symlinks that should escape our scope.
    assertOutOfScopeLink("olink1", "../target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("dir/olink2", "../../target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("olink3", "../" + scopeBase + "/target", scopeRoot + "/target", d);
    assertOutOfScopeLink("dir/dir2/olink5", "../../../target", chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("dir/dir2/olink6", "../dir2/../../dir/dir2/../../../target",
        chopScopeRoot(1) + "/target", d);
    assertOutOfScopeLink("dir/olink7", "../../../target", chopScopeRoot(2) + "target", d);
    assertOutOfScopeLink("olink8", "../../../../../target", "/target", d);

    // In-scope symlink that's not the final segment in a query.
    Path iDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/ilinkdir"));
    testFS.createSymbolicLink(iDirLink, new PathFragment("../../dir"));
    iDirLink.getRelative("file").exists();
    assertNull(d.lastPath());

    // Out-of-scope symlink that's not the final segment in a query.
    Path oDirLink = testFS.getPath(SCOPE_ROOT.getRelative("dir/dir2/olinkdir"));
    testFS.createSymbolicLink(oDirLink, new PathFragment("../../../other/dir"));
    oDirLink.getRelative("file").exists();
    assertEquals(chopScopeRoot(1) + "/other/dir/file", d.lastPath().getPathString());
  }
}
