/* * Copyright (C) 2013 The Guava Authors * * 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.io; import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE; import static com.google.common.jimfs.Feature.SECURE_DIRECTORY_STREAM; import static com.google.common.jimfs.Feature.SYMBOLIC_LINKS; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import com.google.common.collect.ObjectArrays; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Feature; import com.google.common.jimfs.Jimfs; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.FileSystemException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.EnumSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import junit.framework.TestCase; import junit.framework.TestSuite; /** * Tests for {@link MoreFiles}. * * @author Colin Decker */ public class MoreFilesTest extends TestCase { public static TestSuite suite() { TestSuite suite = new TestSuite(); suite.addTest(ByteSourceTester.tests("MoreFiles.asByteSource[Path]", SourceSinkFactories.pathByteSourceFactory(), true)); suite.addTest(ByteSinkTester.tests("MoreFiles.asByteSink[Path]", SourceSinkFactories.pathByteSinkFactory())); suite.addTest(ByteSinkTester.tests("MoreFiles.asByteSink[Path, APPEND]", SourceSinkFactories.appendingPathByteSinkFactory())); suite.addTest(CharSourceTester.tests("MoreFiles.asCharSource[Path, Charset]", SourceSinkFactories.pathCharSourceFactory(), false)); suite.addTest(CharSinkTester.tests("MoreFiles.asCharSink[Path, Charset]", SourceSinkFactories.pathCharSinkFactory())); suite.addTest(CharSinkTester.tests("MoreFiles.asCharSink[Path, Charset, APPEND]", SourceSinkFactories.appendingPathCharSinkFactory())); suite.addTestSuite(MoreFilesTest.class); return suite; } private static final FileSystem FS = FileSystems.getDefault(); private static Path root() { return FS.getRootDirectories().iterator().next(); } private Path tempDir; @Override protected void setUp() throws Exception { tempDir = Files.createTempDirectory("MoreFilesTest"); } @Override protected void tearDown() throws Exception { if (tempDir != null) { // delete tempDir and its contents Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.deleteIfExists(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (exc != null) { return FileVisitResult.TERMINATE; } Files.deleteIfExists(dir); return FileVisitResult.CONTINUE; } }); } } private Path createTempFile() throws IOException { return Files.createTempFile(tempDir, "test", ".test"); } public void testByteSource_size_ofDirectory() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path dir = fs.getPath("dir"); Files.createDirectory(dir); ByteSource source = MoreFiles.asByteSource(dir); assertThat(source.sizeIfKnown()).isAbsent(); try { source.size(); fail(); } catch (IOException expected) { } } } public void testByteSource_size_ofSymlinkToDirectory() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path dir = fs.getPath("dir"); Files.createDirectory(dir); Path link = fs.getPath("link"); Files.createSymbolicLink(link, dir); ByteSource source = MoreFiles.asByteSource(link); assertThat(source.sizeIfKnown()).isAbsent(); try { source.size(); fail(); } catch (IOException expected) { } } } public void testByteSource_size_ofSymlinkToRegularFile() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path file = fs.getPath("file"); Files.write(file, new byte[10]); Path link = fs.getPath("link"); Files.createSymbolicLink(link, file); ByteSource source = MoreFiles.asByteSource(link); assertEquals(10L, (long) source.sizeIfKnown().get()); assertEquals(10L, source.size()); } } public void testByteSource_size_ofSymlinkToRegularFile_nofollowLinks() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path file = fs.getPath("file"); Files.write(file, new byte[10]); Path link = fs.getPath("link"); Files.createSymbolicLink(link, file); ByteSource source = MoreFiles.asByteSource(link, NOFOLLOW_LINKS); assertThat(source.sizeIfKnown()).isAbsent(); try { source.size(); fail(); } catch (IOException expected) { } } } public void testEqual() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path fooPath = fs.getPath("foo"); Path barPath = fs.getPath("bar"); MoreFiles.asCharSink(fooPath, UTF_8).write("foo"); MoreFiles.asCharSink(barPath, UTF_8).write("barbar"); assertThat(MoreFiles.equal(fooPath, barPath)).isFalse(); assertThat(MoreFiles.equal(fooPath, fooPath)).isTrue(); assertThat(MoreFiles.asByteSource(fooPath).contentEquals(MoreFiles.asByteSource(fooPath))) .isTrue(); Path fooCopy = Files.copy(fooPath, fs.getPath("fooCopy")); assertThat(Files.isSameFile(fooPath, fooCopy)).isFalse(); assertThat(MoreFiles.equal(fooPath, fooCopy)).isTrue(); MoreFiles.asCharSink(fooCopy, UTF_8).write("boo"); assertThat(MoreFiles.asByteSource(fooPath).size()) .isEqualTo(MoreFiles.asByteSource(fooCopy).size()); assertThat(MoreFiles.equal(fooPath, fooCopy)).isFalse(); // should also assert that a Path that erroneously reports a size 0 can still be compared, // not sure how to do that with the Path API } } public void testEqual_links() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path fooPath = fs.getPath("foo"); MoreFiles.asCharSink(fooPath, UTF_8).write("foo"); Path fooSymlink = fs.getPath("symlink"); Files.createSymbolicLink(fooSymlink, fooPath); Path fooHardlink = fs.getPath("hardlink"); Files.createLink(fooHardlink, fooPath); assertThat(MoreFiles.equal(fooPath, fooSymlink)).isTrue(); assertThat(MoreFiles.equal(fooPath, fooHardlink)).isTrue(); assertThat(MoreFiles.equal(fooSymlink, fooHardlink)).isTrue(); } } public void testTouch() throws IOException { Path temp = createTempFile(); assertTrue(Files.exists(temp)); Files.delete(temp); assertFalse(Files.exists(temp)); MoreFiles.touch(temp); assertTrue(Files.exists(temp)); MoreFiles.touch(temp); assertTrue(Files.exists(temp)); } public void testTouchTime() throws IOException { Path temp = createTempFile(); assertTrue(Files.exists(temp)); Files.setLastModifiedTime(temp, FileTime.fromMillis(0)); assertEquals(0, Files.getLastModifiedTime(temp).toMillis()); MoreFiles.touch(temp); assertThat(Files.getLastModifiedTime(temp).toMillis()).isNotEqualTo(0); } public void testCreateParentDirectories_root() throws IOException { Path root = root(); assertNull(root.getParent()); assertNull(root.toRealPath().getParent()); MoreFiles.createParentDirectories(root); // test that there's no exception } public void testCreateParentDirectories_relativePath() throws IOException { Path path = FS.getPath("nonexistent.file"); assertNull(path.getParent()); assertNotNull(path.toAbsolutePath().getParent()); MoreFiles.createParentDirectories(path); // test that there's no exception } public void testCreateParentDirectories_noParentsNeeded() throws IOException { Path path = tempDir.resolve("nonexistent.file"); assertTrue(Files.exists(path.getParent())); MoreFiles.createParentDirectories(path); // test that there's no exception } public void testCreateParentDirectories_oneParentNeeded() throws IOException { Path path = tempDir.resolve("parent/nonexistent.file"); Path parent = path.getParent(); assertFalse(Files.exists(parent)); MoreFiles.createParentDirectories(path); assertTrue(Files.exists(parent)); } public void testCreateParentDirectories_multipleParentsNeeded() throws IOException { Path path = tempDir.resolve("grandparent/parent/nonexistent.file"); Path parent = path.getParent(); Path grandparent = parent.getParent(); assertFalse(Files.exists(grandparent)); assertFalse(Files.exists(parent)); MoreFiles.createParentDirectories(path); assertTrue(Files.exists(parent)); assertTrue(Files.exists(grandparent)); } public void testCreateParentDirectories_noPermission() { Path file = root().resolve("parent/nonexistent.file"); Path parent = file.getParent(); assertFalse(Files.exists(parent)); try { MoreFiles.createParentDirectories(file); // Cleanup in case parent creation was [erroneously] successful. Files.delete(parent); fail("expected exception"); } catch (IOException expected) { } } public void testCreateParentDirectories_nonDirectoryParentExists() throws IOException { Path parent = createTempFile(); assertTrue(Files.isRegularFile(parent)); Path file = parent.resolve("foo"); try { MoreFiles.createParentDirectories(file); fail(); } catch (IOException expected) { } } public void testCreateParentDirectories_symlinkParentExists() throws IOException { Path symlink = tempDir.resolve("linkToDir"); Files.createSymbolicLink(symlink, root()); Path file = symlink.resolve("foo"); MoreFiles.createParentDirectories(file); } public void testGetFileExtension() { assertEquals("txt", MoreFiles.getFileExtension(FS.getPath(".txt"))); assertEquals("txt", MoreFiles.getFileExtension(FS.getPath("blah.txt"))); assertEquals("txt", MoreFiles.getFileExtension(FS.getPath("blah..txt"))); assertEquals("txt", MoreFiles.getFileExtension(FS.getPath(".blah.txt"))); assertEquals("txt", MoreFiles.getFileExtension(root().resolve("tmp/blah.txt"))); assertEquals("gz", MoreFiles.getFileExtension(FS.getPath("blah.tar.gz"))); assertEquals("", MoreFiles.getFileExtension(root())); assertEquals("", MoreFiles.getFileExtension(FS.getPath("."))); assertEquals("", MoreFiles.getFileExtension(FS.getPath(".."))); assertEquals("", MoreFiles.getFileExtension(FS.getPath("..."))); assertEquals("", MoreFiles.getFileExtension(FS.getPath("blah"))); assertEquals("", MoreFiles.getFileExtension(FS.getPath("blah."))); assertEquals("", MoreFiles.getFileExtension(FS.getPath(".blah."))); assertEquals("", MoreFiles.getFileExtension(root().resolve("foo.bar/blah"))); assertEquals("", MoreFiles.getFileExtension(root().resolve("foo/.bar/blah"))); } public void testGetNameWithoutExtension() { assertEquals("", MoreFiles.getNameWithoutExtension(FS.getPath(".txt"))); assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah.txt"))); assertEquals("blah.", MoreFiles.getNameWithoutExtension(FS.getPath("blah..txt"))); assertEquals(".blah", MoreFiles.getNameWithoutExtension(FS.getPath(".blah.txt"))); assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("tmp/blah.txt"))); assertEquals("blah.tar", MoreFiles.getNameWithoutExtension(FS.getPath("blah.tar.gz"))); assertEquals("", MoreFiles.getNameWithoutExtension(root())); assertEquals("", MoreFiles.getNameWithoutExtension(FS.getPath("."))); assertEquals(".", MoreFiles.getNameWithoutExtension(FS.getPath(".."))); assertEquals("..", MoreFiles.getNameWithoutExtension(FS.getPath("..."))); assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah"))); assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah."))); assertEquals(".blah", MoreFiles.getNameWithoutExtension(FS.getPath(".blah."))); assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("foo.bar/blah"))); assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("foo/.bar/blah"))); } public void testPredicates() throws IOException { Path file = createTempFile(); Path dir = tempDir.resolve("dir"); Files.createDirectory(dir); assertTrue(MoreFiles.isDirectory().apply(dir)); assertFalse(MoreFiles.isRegularFile().apply(dir)); assertFalse(MoreFiles.isDirectory().apply(file)); assertTrue(MoreFiles.isRegularFile().apply(file)); Path symlinkToDir = tempDir.resolve("symlinkToDir"); Path symlinkToFile = tempDir.resolve("symlinkToFile"); Files.createSymbolicLink(symlinkToDir, dir); Files.createSymbolicLink(symlinkToFile, file); assertTrue(MoreFiles.isDirectory().apply(symlinkToDir)); assertFalse(MoreFiles.isRegularFile().apply(symlinkToDir)); assertFalse(MoreFiles.isDirectory().apply(symlinkToFile)); assertTrue(MoreFiles.isRegularFile().apply(symlinkToFile)); assertFalse(MoreFiles.isDirectory(NOFOLLOW_LINKS).apply(symlinkToDir)); assertFalse(MoreFiles.isRegularFile(NOFOLLOW_LINKS).apply(symlinkToFile)); } /** * Creates a new file system for testing that supports the given features in addition to * supporting symbolic links. The file system is created initially having the following file * structure: * * <pre> * / * work/ * dir/ * a * b/ * g * h -> ../a * i/ * j/ * k * l/ * c * d -> b/i * e/ * f -> /dontdelete * dontdelete/ * a * b/ * c * symlinktodir -> work/dir * </pre> */ static FileSystem newTestFileSystem(Feature... supportedFeatures) throws IOException { FileSystem fs = Jimfs.newFileSystem(Configuration.unix().toBuilder() .setSupportedFeatures(ObjectArrays.concat(SYMBOLIC_LINKS, supportedFeatures)) .build()); Files.createDirectories(fs.getPath("dir/b/i/j/l")); Files.createFile(fs.getPath("dir/a")); Files.createFile(fs.getPath("dir/c")); Files.createSymbolicLink(fs.getPath("dir/d"), fs.getPath("b/i")); Files.createDirectory(fs.getPath("dir/e")); Files.createSymbolicLink(fs.getPath("dir/f"), fs.getPath("/dontdelete")); Files.createFile(fs.getPath("dir/b/g")); Files.createSymbolicLink(fs.getPath("dir/b/h"), fs.getPath("../a")); Files.createFile(fs.getPath("dir/b/i/j/k")); Files.createDirectory(fs.getPath("/dontdelete")); Files.createFile(fs.getPath("/dontdelete/a")); Files.createDirectory(fs.getPath("/dontdelete/b")); Files.createFile(fs.getPath("/dontdelete/c")); Files.createSymbolicLink(fs.getPath("/symlinktodir"), fs.getPath("work/dir")); return fs; } public void testDirectoryDeletion_basic() throws IOException { for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); method.delete(dir); method.assertDeleteSucceeded(dir); assertEquals("contents of /dontdelete deleted by delete method " + method, 3, MoreFiles.listFiles(fs.getPath("/dontdelete")).size()); } } } public void testDirectoryDeletion_emptyDir() throws IOException { for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path emptyDir = fs.getPath("dir/e"); assertEquals(0, MoreFiles.listFiles(emptyDir).size()); method.delete(emptyDir); method.assertDeleteSucceeded(emptyDir); } } } public void testDeleteRecursively_symlinkToDir() throws IOException { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path symlink = fs.getPath("/symlinktodir"); Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); MoreFiles.deleteRecursively(symlink); assertFalse(Files.exists(symlink)); assertTrue(Files.exists(dir)); assertEquals(6, MoreFiles.listFiles(dir).size()); } } public void testDeleteDirectoryContents_symlinkToDir() throws IOException { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path symlink = fs.getPath("/symlinktodir"); Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(symlink).size()); MoreFiles.deleteDirectoryContents(symlink); assertTrue(Files.exists(symlink, NOFOLLOW_LINKS)); assertTrue(Files.exists(symlink)); assertTrue(Files.exists(dir)); assertEquals(0, MoreFiles.listFiles(symlink).size()); } } public void testDirectoryDeletion_sdsNotSupported_fails() throws IOException { for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) { try (FileSystem fs = newTestFileSystem()) { Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); try { method.delete(dir); fail("expected InsecureRecursiveDeleteException"); } catch (InsecureRecursiveDeleteException expected) { } assertTrue(Files.exists(dir)); assertEquals(6, MoreFiles.listFiles(dir).size()); } } } public void testDirectoryDeletion_sdsNotSupported_allowInsecure() throws IOException { for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) { try (FileSystem fs = newTestFileSystem()) { Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); method.delete(dir, ALLOW_INSECURE); method.assertDeleteSucceeded(dir); assertEquals("contents of /dontdelete deleted by delete method " + method, 3, MoreFiles.listFiles(fs.getPath("/dontdelete")).size()); } } } public void testDeleteRecursively_symlinkToDir_sdsNotSupported_allowInsecure() throws IOException { try (FileSystem fs = newTestFileSystem()) { Path symlink = fs.getPath("/symlinktodir"); Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); MoreFiles.deleteRecursively(symlink, ALLOW_INSECURE); assertFalse(Files.exists(symlink)); assertTrue(Files.exists(dir)); assertEquals(6, MoreFiles.listFiles(dir).size()); } } public void testDeleteDirectoryContents_symlinkToDir_sdsNotSupported_allowInsecure() throws IOException { try (FileSystem fs = newTestFileSystem()) { Path symlink = fs.getPath("/symlinktodir"); Path dir = fs.getPath("dir"); assertEquals(6, MoreFiles.listFiles(dir).size()); MoreFiles.deleteDirectoryContents(symlink, ALLOW_INSECURE); assertEquals(0, MoreFiles.listFiles(dir).size()); } } /** * This test attempts to create a situation in which one thread is constantly changing a file * from being a real directory to being a symlink to another directory. It then calls * deleteDirectoryContents thousands of times on a directory whose subtree contains the file * that's switching between directory and symlink to try to ensure that under no circumstance * does deleteDirectoryContents follow the symlink to the other directory and delete that * directory's contents. * * <p>We can only test this with a file system that supports SecureDirectoryStream, because it's * not possible to protect against this if the file system doesn't. */ public void testDirectoryDeletion_directorySymlinkRace() throws IOException { for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path dirToDelete = fs.getPath("dir/b/i"); Path changingFile = dirToDelete.resolve("j/l"); Path symlinkTarget = fs.getPath("/dontdelete"); ExecutorService executor = Executors.newSingleThreadExecutor(); startDirectorySymlinkSwitching(changingFile, symlinkTarget, executor); try { for (int i = 0; i < 5000; i++) { try { Files.createDirectories(changingFile); Files.createFile(dirToDelete.resolve("j/k")); } catch (FileAlreadyExistsException expected) { // if a file already exists, that's fine... just continue } try { method.delete(dirToDelete); } catch (FileSystemException expected) { // the delete method may or may not throw an exception, but if it does that's fine // and expected } // this test is mainly checking that the contents of /dontdelete aren't deleted under // any circumstances assertEquals(3, MoreFiles.listFiles(symlinkTarget).size()); Thread.yield(); } } finally { executor.shutdownNow(); } } } } public void testDeleteRecursively_nonDirectoryFile() throws IOException { try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) { Path file = fs.getPath("dir/a"); assertTrue(Files.isRegularFile(file, NOFOLLOW_LINKS)); MoreFiles.deleteRecursively(file); assertFalse(Files.exists(file, NOFOLLOW_LINKS)); Path symlink = fs.getPath("/symlinktodir"); assertTrue(Files.isSymbolicLink(symlink)); Path realSymlinkTarget = symlink.toRealPath(); assertTrue(Files.isDirectory(realSymlinkTarget, NOFOLLOW_LINKS)); MoreFiles.deleteRecursively(symlink); assertFalse(Files.exists(symlink, NOFOLLOW_LINKS)); assertTrue(Files.isDirectory(realSymlinkTarget, NOFOLLOW_LINKS)); } } /** * Starts a new task on the given executor that switches (deletes and replaces) a file between * being a directory and being a symlink. The given {@code file} is the file that should switch * between being a directory and being a symlink, while the given {@code target} is the target * the symlink should have. */ private static void startDirectorySymlinkSwitching( final Path file, final Path target, ExecutorService executor) { @SuppressWarnings("unused") // go/futurereturn-lsc Future<?> possiblyIgnoredError = executor.submit( new Runnable() { @Override public void run() { boolean createSymlink = false; while (!Thread.interrupted()) { try { // trying to switch between a real directory and a symlink (dir -> /a) if (Files.deleteIfExists(file)) { if (createSymlink) { Files.createSymbolicLink(file, target); } else { Files.createDirectory(file); } createSymlink = !createSymlink; } } catch (IOException tolerated) { // it's expected that some of these will fail } Thread.yield(); } } }); } /** * Enum defining the two MoreFiles methods that delete directory contents. */ private enum DirectoryDeleteMethod { DELETE_DIRECTORY_CONTENTS { @Override public void delete(Path path, RecursiveDeleteOption... options) throws IOException { MoreFiles.deleteDirectoryContents(path, options); } @Override public void assertDeleteSucceeded(Path path) throws IOException { assertEquals("contents of directory " + path + " not deleted with delete method " + this, 0, MoreFiles.listFiles(path).size()); } }, DELETE_RECURSIVELY { @Override public void delete(Path path, RecursiveDeleteOption... options) throws IOException { MoreFiles.deleteRecursively(path, options); } @Override public void assertDeleteSucceeded(Path path) throws IOException { assertFalse("file " + path + " not deleted with delete method " + this, Files.exists(path)); } }; public abstract void delete(Path path, RecursiveDeleteOption... options) throws IOException; public abstract void assertDeleteSucceeded(Path path) throws IOException; } }