/* * Copyright 2013 Google Inc. * * 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.common.jimfs; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.jimfs.TestUtils.regularFile; import static com.google.common.truth.Truth.assertThat; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static org.junit.Assert.fail; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import java.io.IOException; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.util.HashMap; import java.util.Map; import java.util.Random; import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests for {@link FileTree}. * * @author Colin Decker */ @RunWith(JUnit4.class) public class FileTreeTest { /* * Directory structure. Each file should have a unique name. * * / * work/ * one/ * two/ * three/ * eleven * four/ * five -> /foo * six -> ../one * loop -> ../four/loop * foo/ * bar/ * $ * a/ * b/ * c/ */ /** * This path service is for unix-like paths, with the exception that it recognizes $ and ! as * roots in addition to /, allowing for up to three roots. When creating a {@linkplain * PathType#toUriPath URI path}, we prefix the path with / to differentiate between a path like * "$foo/bar" and one like "/$foo/bar". They would become "/$foo/bar" and "//$foo/bar" * respectively. */ private final PathService pathService = PathServiceTest.fakePathService( new PathType(true, '/') { @Override public ParseResult parsePath(String path) { String root = null; if (path.matches("^[/$!].*")) { root = path.substring(0, 1); path = path.substring(1); } return new ParseResult(root, Splitter.on('/').omitEmptyStrings().split(path)); } @Override public String toString(@NullableDecl String root, Iterable names) { root = Strings.nullToEmpty(root); return root + Joiner.on('/').join(names); } @Override public String toUriPath(String root, Iterable names, boolean directory) { // need to add extra / to differentiate between paths "/$foo/bar" and "$foo/bar". return "/" + toString(root, names); } @Override public ParseResult parseUriPath(String uriPath) { checkArgument( uriPath.matches("^/[/$!].*"), "uriPath (%s) must start with // or /$ or /!"); return parsePath(uriPath.substring(1)); // skip leading / } }, false); private FileTree fileTree; private File workingDirectory; private final Map files = new HashMap<>(); @Before public void setUp() { Directory root = Directory.createRoot(0, Name.simple("/")); files.put("/", root); Directory otherRoot = Directory.createRoot(2, Name.simple("$")); files.put("$", otherRoot); Map roots = new HashMap<>(); roots.put(Name.simple("/"), root); roots.put(Name.simple("$"), otherRoot); fileTree = new FileTree(roots); workingDirectory = createDirectory("/", "work"); createDirectory("work", "one"); createDirectory("one", "two"); createFile("one", "eleven"); createDirectory("two", "three"); createDirectory("work", "four"); createSymbolicLink("four", "five", "/foo"); createSymbolicLink("four", "six", "../one"); createSymbolicLink("four", "loop", "../four/loop"); createDirectory("/", "foo"); createDirectory("foo", "bar"); createDirectory("$", "a"); createDirectory("a", "b"); createDirectory("b", "c"); } // absolute lookups @Test public void testLookup_root() throws IOException { assertExists(lookup("/"), "/", "/"); assertExists(lookup("$"), "$", "$"); } @Test public void testLookup_nonExistentRoot() throws IOException { try { lookup("!"); fail(); } catch (NoSuchFileException expected) { } try { lookup("!a"); fail(); } catch (NoSuchFileException expected) { } } @Test public void testLookup_absolute() throws IOException { assertExists(lookup("/work"), "/", "work"); assertExists(lookup("/work/one/two/three"), "two", "three"); assertExists(lookup("$a"), "$", "a"); assertExists(lookup("$a/b/c"), "b", "c"); } @Test public void testLookup_absolute_notExists() throws IOException { try { lookup("/a/b"); fail(); } catch (NoSuchFileException expected) { } try { lookup("/work/one/foo/bar"); fail(); } catch (NoSuchFileException expected) { } try { lookup("$c/d"); fail(); } catch (NoSuchFileException expected) { } try { lookup("$a/b/c/d/e"); fail(); } catch (NoSuchFileException expected) { } } @Test public void testLookup_absolute_parentExists() throws IOException { assertParentExists(lookup("/a"), "/"); assertParentExists(lookup("/foo/baz"), "foo"); assertParentExists(lookup("$c"), "$"); assertParentExists(lookup("$a/b/c/d"), "c"); } @Test public void testLookup_absolute_nonDirectoryIntermediateFile() throws IOException { try { lookup("/work/one/eleven/twelve"); fail(); } catch (NoSuchFileException expected) { } try { lookup("/work/one/eleven/twelve/thirteen/fourteen"); fail(); } catch (NoSuchFileException expected) { } } @Test public void testLookup_absolute_intermediateSymlink() throws IOException { assertExists(lookup("/work/four/five/bar"), "foo", "bar"); assertExists(lookup("/work/four/six/two/three"), "two", "three"); // NOFOLLOW_LINKS doesn't affect intermediate symlinks assertExists(lookup("/work/four/five/bar", NOFOLLOW_LINKS), "foo", "bar"); assertExists(lookup("/work/four/six/two/three", NOFOLLOW_LINKS), "two", "three"); } @Test public void testLookup_absolute_intermediateSymlink_parentExists() throws IOException { assertParentExists(lookup("/work/four/five/baz"), "foo"); assertParentExists(lookup("/work/four/six/baz"), "one"); } @Test public void testLookup_absolute_finalSymlink() throws IOException { assertExists(lookup("/work/four/five"), "/", "foo"); assertExists(lookup("/work/four/six"), "work", "one"); } @Test public void testLookup_absolute_finalSymlink_nofollowLinks() throws IOException { assertExists(lookup("/work/four/five", NOFOLLOW_LINKS), "four", "five"); assertExists(lookup("/work/four/six", NOFOLLOW_LINKS), "four", "six"); assertExists(lookup("/work/four/loop", NOFOLLOW_LINKS), "four", "loop"); } @Test public void testLookup_absolute_symlinkLoop() { try { lookup("/work/four/loop"); fail(); } catch (IOException expected) { } try { lookup("/work/four/loop/whatever"); fail(); } catch (IOException expected) { } } @Test public void testLookup_absolute_withDotsInPath() throws IOException { assertExists(lookup("/."), "/", "/"); assertExists(lookup("/./././."), "/", "/"); assertExists(lookup("/work/./one/./././two/three"), "two", "three"); assertExists(lookup("/work/./one/./././two/././three"), "two", "three"); assertExists(lookup("/work/./one/./././two/three/././."), "two", "three"); } @Test public void testLookup_absolute_withDotDotsInPath() throws IOException { assertExists(lookup("/.."), "/", "/"); assertExists(lookup("/../../.."), "/", "/"); assertExists(lookup("/work/.."), "/", "/"); assertExists(lookup("/work/../work/one/two/../two/three"), "two", "three"); assertExists(lookup("/work/one/two/../../four/../one/two/three/../three"), "two", "three"); assertExists(lookup("/work/one/two/three/../../two/three/.."), "one", "two"); assertExists(lookup("/work/one/two/three/../../two/three/../.."), "work", "one"); } @Test public void testLookup_absolute_withDotDotsInPath_afterSymlink() throws IOException { assertExists(lookup("/work/four/five/.."), "/", "/"); assertExists(lookup("/work/four/six/.."), "/", "work"); } // relative lookups @Test public void testLookup_relative() throws IOException { assertExists(lookup("one"), "work", "one"); assertExists(lookup("one/two/three"), "two", "three"); } @Test public void testLookup_relative_notExists() throws IOException { try { lookup("a/b"); fail(); } catch (NoSuchFileException expected) { } try { lookup("one/foo/bar"); fail(); } catch (NoSuchFileException expected) { } } @Test public void testLookup_relative_parentExists() throws IOException { assertParentExists(lookup("a"), "work"); assertParentExists(lookup("one/two/four"), "two"); } @Test public void testLookup_relative_nonDirectoryIntermediateFile() throws IOException { try { lookup("one/eleven/twelve"); fail(); } catch (NoSuchFileException expected) { } try { lookup("one/eleven/twelve/thirteen/fourteen"); fail(); } catch (NoSuchFileException expected) { } } @Test public void testLookup_relative_intermediateSymlink() throws IOException { assertExists(lookup("four/five/bar"), "foo", "bar"); assertExists(lookup("four/six/two/three"), "two", "three"); // NOFOLLOW_LINKS doesn't affect intermediate symlinks assertExists(lookup("four/five/bar", NOFOLLOW_LINKS), "foo", "bar"); assertExists(lookup("four/six/two/three", NOFOLLOW_LINKS), "two", "three"); } @Test public void testLookup_relative_intermediateSymlink_parentExists() throws IOException { assertParentExists(lookup("four/five/baz"), "foo"); assertParentExists(lookup("four/six/baz"), "one"); } @Test public void testLookup_relative_finalSymlink() throws IOException { assertExists(lookup("four/five"), "/", "foo"); assertExists(lookup("four/six"), "work", "one"); } @Test public void testLookup_relative_finalSymlink_nofollowLinks() throws IOException { assertExists(lookup("four/five", NOFOLLOW_LINKS), "four", "five"); assertExists(lookup("four/six", NOFOLLOW_LINKS), "four", "six"); assertExists(lookup("four/loop", NOFOLLOW_LINKS), "four", "loop"); } @Test public void testLookup_relative_symlinkLoop() { try { lookup("four/loop"); fail(); } catch (IOException expected) { } try { lookup("four/loop/whatever"); fail(); } catch (IOException expected) { } } @Test public void testLookup_relative_emptyPath() throws IOException { assertExists(lookup(""), "/", "work"); } @Test public void testLookup_relative_withDotsInPath() throws IOException { assertExists(lookup("."), "/", "work"); assertExists(lookup("././."), "/", "work"); assertExists(lookup("./one/./././two/three"), "two", "three"); assertExists(lookup("./one/./././two/././three"), "two", "three"); assertExists(lookup("./one/./././two/three/././."), "two", "three"); } @Test public void testLookup_relative_withDotDotsInPath() throws IOException { assertExists(lookup(".."), "/", "/"); assertExists(lookup("../../.."), "/", "/"); assertExists(lookup("../work"), "/", "work"); assertExists(lookup("../../work"), "/", "work"); assertExists(lookup("../foo"), "/", "foo"); assertExists(lookup("../work/one/two/../two/three"), "two", "three"); assertExists(lookup("one/two/../../four/../one/two/three/../three"), "two", "three"); assertExists(lookup("one/two/three/../../two/three/.."), "one", "two"); assertExists(lookup("one/two/three/../../two/three/../.."), "work", "one"); } @Test public void testLookup_relative_withDotDotsInPath_afterSymlink() throws IOException { assertExists(lookup("four/five/.."), "/", "/"); assertExists(lookup("four/six/.."), "/", "work"); } private DirectoryEntry lookup(String path, LinkOption... options) throws IOException { JimfsPath pathObj = pathService.parsePath(path); return fileTree.lookUp(workingDirectory, pathObj, Options.getLinkOptions(options)); } private void assertExists(DirectoryEntry entry, String parent, String file) { assertThat(entry.exists()).isTrue(); assertThat(entry.name()).isEqualTo(Name.simple(file)); assertThat(entry.directory()).isEqualTo(files.get(parent)); assertThat(entry.file()).isEqualTo(files.get(file)); } private void assertParentExists(DirectoryEntry entry, String parent) { assertThat(entry.exists()).isFalse(); assertThat(entry.directory()).isEqualTo(files.get(parent)); try { entry.file(); fail(); } catch (IllegalStateException expected) { } } private File createDirectory(String parent, String name) { Directory dir = (Directory) files.get(parent); Directory newFile = Directory.create(new Random().nextInt()); dir.link(Name.simple(name), newFile); files.put(name, newFile); return newFile; } private File createFile(String parent, String name) { Directory dir = (Directory) files.get(parent); File newFile = regularFile(0); dir.link(Name.simple(name), newFile); files.put(name, newFile); return newFile; } private File createSymbolicLink(String parent, String name, String target) { Directory dir = (Directory) files.get(parent); File newFile = SymbolicLink.create(new Random().nextInt(), pathService.parsePath(target)); dir.link(Name.simple(name), newFile); files.put(name, newFile); return newFile; } }