/* * 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.base.StandardSystemProperty.OS_NAME; 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 static org.junit.Assert.assertThrows; 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.NoSuchFileException; 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}. * *
Note: {@link MoreFiles#fileTraverser()} is tested in {@link MoreFilesFileTraverserTest}.
*
* @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 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 {
int iterations = isAndroid() ? 100 : 5000;
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 < iterations; 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") // https://errorprone.info/bugpattern/FutureReturnValueIgnored
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;
}
private static boolean isWindows() {
return OS_NAME.value().startsWith("Windows");
}
private static boolean isAndroid() {
return System.getProperty("java.runtime.name", "").contains("Android");
}
}
* /
* 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
*
*/
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());
assertThrows(InsecureRecursiveDeleteException.class, () -> method.delete(dir));
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 testDeleteRecursively_nonexistingFile_throwsNoSuchFileException() throws IOException {
try (FileSystem fs = newTestFileSystem()) {
NoSuchFileException expected =
assertThrows(
NoSuchFileException.class,
() -> MoreFiles.deleteRecursively(fs.getPath("/work/nothere"), ALLOW_INSECURE));
assertThat(expected.getFile()).isEqualTo("/work/nothere");
}
}
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.
*
*