diff options
Diffstat (limited to 'src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java')
-rw-r--r-- | src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java | 806 |
1 files changed, 806 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java new file mode 100644 index 0000000000..6c8071f075 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/vfs/ScopeEscapableFileSystemTest.java @@ -0,0 +1,806 @@ +// Copyright 2014 Google Inc. 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 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.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; + +/** + * 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 supportsSymbolicLinks() { 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 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 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; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + Preconditions.checkState(testFS instanceof ScopeEscapableFileSystem, + "Not ScopeEscapable: " + 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) { + assertTrue(expectedDelegatedPath.equals(delegator.lastPath())); + } + + // 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()); + assertTrue(delegator.lastPath() == null); + + 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()); + assertTrue(delegator.lastPath() == null); // 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")); + assertEquals(null, delegator.lastPath()); // Renaming a link shouldn't require delegation. + assertEquals(null, 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(); + assertEquals(null, 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())); + InputStream is = fileLink.getInputStream(); + assertEquals(fileLinkTarget, delegator.lastPath()); + assertSame(delegator.objectState(), is); + + delegator.setState(new ByteArrayInputStream("blah2".getBytes())); + 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()); + assertEquals(1, entries.size()); + 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()); + } +} |