diff --git a/android/guava-tests/test/com/google/common/io/MoreFilesTest.java b/android/guava-tests/test/com/google/common/io/MoreFilesTest.java new file mode 100644 index 000000000000..a8c041bab6f8 --- /dev/null +++ b/android/guava-tests/test/com/google/common/io/MoreFilesTest.java @@ -0,0 +1,709 @@ +/* + * 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; + +/** + * Tests for {@link MoreFiles}. + * + *
Note: {@link MoreFiles#fileTraverser()} is tested in {@link MoreFilesFileTraverserTest}.
+ *
+ * @author Colin Decker
+ */
+
+public class MoreFilesTest extends TestCase {
+
+ /*
+ * Note: We don't include suite() in the backport. I've lost track of whether the Android test
+ * runner would run it even if we did, but part of the problem is b/230620681.
+ */
+
+ 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.
+ */
+ @SuppressWarnings("ThreadPriorityCheck") // TODO: b/175898629 - Consider onSpinWait.
+ 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.
+ */
+ @SuppressWarnings("ThreadPriorityCheck") // TODO: b/175898629 - Consider onSpinWait.
+ 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");
+ }
+}
diff --git a/android/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java b/android/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java
new file mode 100644
index 000000000000..83c7e72e831b
--- /dev/null
+++ b/android/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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 com.google.common.annotations.GwtIncompatible;
+import com.google.common.annotations.J2ktIncompatible;
+import com.google.j2objc.annotations.J2ObjCIncompatible;
+import java.nio.file.FileSystemException;
+import java.nio.file.SecureDirectoryStream;
+import javax.annotation.CheckForNull;
+
+/**
+ * Exception indicating that a recursive delete can't be performed because the file system does not
+ * have the support necessary to guarantee that it is not vulnerable to race conditions that would
+ * allow it to delete files and directories outside of the directory being deleted (i.e., {@link
+ * SecureDirectoryStream} is not supported).
+ *
+ * {@link RecursiveDeleteOption#ALLOW_INSECURE} can be used to force the recursive delete method
+ * to proceed anyway.
+ *
+ * @since NEXT (but since 21.0 in the JRE flavor)
+ * @author Colin Decker
+ */
+@J2ktIncompatible
+@GwtIncompatible
+@J2ObjCIncompatible // java.nio.file
+@ElementTypesAreNonnullByDefault
+// Users are unlikely to use this unless they're already interacting with MoreFiles and Path.
+@IgnoreJRERequirement
+public final class InsecureRecursiveDeleteException extends FileSystemException {
+
+ public InsecureRecursiveDeleteException(@CheckForNull String file) {
+ super(file, null, "unable to guarantee security of recursive delete");
+ }
+}
diff --git a/android/guava/src/com/google/common/io/MoreFiles.java b/android/guava/src/com/google/common/io/MoreFiles.java
new file mode 100644
index 000000000000..a2bd644e4478
--- /dev/null
+++ b/android/guava/src/com/google/common/io/MoreFiles.java
@@ -0,0 +1,874 @@
+/*
+ * 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.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.annotations.J2ktIncompatible;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.graph.Traverser;
+import com.google.j2objc.annotations.J2ObjCIncompatible;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.SecureDirectoryStream;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+
+/**
+ * Static utilities for use with {@link Path} instances, intended to complement {@link Files}.
+ *
+ * Many methods provided by Guava's {@code Files} class for {@link java.io.File} instances are
+ * now available via the JDK's {@link java.nio.file.Files} class for {@code Path} - check the JDK's
+ * class if a sibling method from {@code Files} appears to be missing from this class.
+ *
+ * @since NEXT (but since 21.0 in the JRE flavor)
+ * @author Colin Decker
+ */
+@J2ktIncompatible
+@GwtIncompatible
+@J2ObjCIncompatible // java.nio.file
+@ElementTypesAreNonnullByDefault
+@IgnoreJRERequirement // Users will use this only if they're already using Path.
+public final class MoreFiles {
+
+ private MoreFiles() {}
+
+ /**
+ * Returns a view of the given {@code path} as a {@link ByteSource}.
+ *
+ * Any {@linkplain OpenOption open options} provided are used when opening streams to the file
+ * and may affect the behavior of the returned source and the streams it provides. See {@link
+ * StandardOpenOption} for the standard options that may be provided. Providing no options is
+ * equivalent to providing the {@link StandardOpenOption#READ READ} option.
+ */
+ public static ByteSource asByteSource(Path path, OpenOption... options) {
+ return new PathByteSource(path, options);
+ }
+
+ @IgnoreJRERequirement // *should* be redundant with the one on MoreFiles itself
+ private static final class PathByteSource extends
+ ByteSource
+ {
+
+ private static final LinkOption[] FOLLOW_LINKS = {};
+
+ private final Path path;
+ private final OpenOption[] options;
+ private final boolean followLinks;
+
+ private PathByteSource(Path path, OpenOption... options) {
+ this.path = checkNotNull(path);
+ this.options = options.clone();
+ this.followLinks = followLinks(this.options);
+ // TODO(cgdecker): validate the provided options... for example, just WRITE seems wrong
+ }
+
+ private static boolean followLinks(OpenOption[] options) {
+ for (OpenOption option : options) {
+ if (option == NOFOLLOW_LINKS) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public InputStream openStream() throws IOException {
+ return Files.newInputStream(path, options);
+ }
+
+ private BasicFileAttributes readAttributes() throws IOException {
+ return Files.readAttributes(
+ path,
+ BasicFileAttributes.class,
+ followLinks ? FOLLOW_LINKS : new LinkOption[] {NOFOLLOW_LINKS});
+ }
+
+ @Override
+ public Optional Any {@linkplain OpenOption open options} provided are used when opening streams to the file
+ * and may affect the behavior of the returned sink and the streams it provides. See {@link
+ * StandardOpenOption} for the standard options that may be provided. Providing no options is
+ * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link
+ * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE
+ * WRITE} options.
+ */
+ public static ByteSink asByteSink(Path path, OpenOption... options) {
+ return new PathByteSink(path, options);
+ }
+
+ @IgnoreJRERequirement // *should* be redundant with the one on MoreFiles itself
+ private static final class PathByteSink extends ByteSink {
+
+ private final Path path;
+ private final OpenOption[] options;
+
+ private PathByteSink(Path path, OpenOption... options) {
+ this.path = checkNotNull(path);
+ this.options = options.clone();
+ // TODO(cgdecker): validate the provided options... for example, just READ seems wrong
+ }
+
+ @Override
+ public OutputStream openStream() throws IOException {
+ return Files.newOutputStream(path, options);
+ }
+
+ @Override
+ public String toString() {
+ return "MoreFiles.asByteSink(" + path + ", " + Arrays.toString(options) + ")";
+ }
+ }
+
+ /**
+ * Returns a view of the given {@code path} as a {@link CharSource} using the given {@code
+ * charset}.
+ *
+ * Any {@linkplain OpenOption open options} provided are used when opening streams to the file
+ * and may affect the behavior of the returned source and the streams it provides. See {@link
+ * StandardOpenOption} for the standard options that may be provided. Providing no options is
+ * equivalent to providing the {@link StandardOpenOption#READ READ} option.
+ */
+ public static CharSource asCharSource(Path path, Charset charset, OpenOption... options) {
+ return asByteSource(path, options).asCharSource(charset);
+ }
+
+ /**
+ * Returns a view of the given {@code path} as a {@link CharSink} using the given {@code charset}.
+ *
+ * Any {@linkplain OpenOption open options} provided are used when opening streams to the file
+ * and may affect the behavior of the returned sink and the streams it provides. See {@link
+ * StandardOpenOption} for the standard options that may be provided. Providing no options is
+ * equivalent to providing the {@link StandardOpenOption#CREATE CREATE}, {@link
+ * StandardOpenOption#TRUNCATE_EXISTING TRUNCATE_EXISTING} and {@link StandardOpenOption#WRITE
+ * WRITE} options.
+ */
+ public static CharSink asCharSink(Path path, Charset charset, OpenOption... options) {
+ return asByteSink(path, options).asCharSink(charset);
+ }
+
+ /**
+ * Returns an immutable list of paths to the files contained in the given directory.
+ *
+ * @throws NoSuchFileException if the file does not exist (optional specific exception)
+ * @throws NotDirectoryException if the file could not be opened because it is not a directory
+ * (optional specific exception)
+ * @throws IOException if an I/O error occurs
+ */
+ public static ImmutableList The returned traverser attempts to avoid following symbolic links to directories. However,
+ * the traverser cannot guarantee that it will not follow symbolic links to directories as it is
+ * possible for a directory to be replaced with a symbolic link between checking if the file is a
+ * directory and actually reading the contents of that directory.
+ *
+ * If the {@link Path} passed to one of the traversal methods does not exist or is not a
+ * directory, no exception will be thrown and the returned {@link Iterable} will contain a single
+ * element: that path.
+ *
+ * {@link DirectoryIteratorException} may be thrown when iterating {@link Iterable} instances
+ * created by this traverser if an {@link IOException} is thrown by a call to {@link
+ * #listFiles(Path)}.
+ *
+ * Example: {@code MoreFiles.fileTraverser().depthFirstPreOrder(Paths.get("/"))} may return the
+ * following paths: {@code ["/", "/etc", "/etc/config.txt", "/etc/fonts", "/home", "/home/alice",
+ * ...]}
+ *
+ * @since 23.5
+ */
+ public static Traverser Note: This method simply returns everything after the last '{@code .}' in the file's
+ * name as determined by {@link Path#getFileName}. It does not account for any filesystem-specific
+ * behavior that the {@link Path} API does not already account for. For example, on NTFS it will
+ * report {@code "txt"} as the extension for the filename {@code "foo.exe:.txt"} even though NTFS
+ * will drop the {@code ":.txt"} part of the name when the file is actually created on the
+ * filesystem due to NTFS's Alternate
+ * Data Streams.
+ */
+ public static String getFileExtension(Path path) {
+ Path name = path.getFileName();
+
+ // null for empty paths and root-only paths
+ if (name == null) {
+ return "";
+ }
+
+ String fileName = name.toString();
+ int dotIndex = fileName.lastIndexOf('.');
+ return dotIndex == -1 ? "" : fileName.substring(dotIndex + 1);
+ }
+
+ /**
+ * Returns the file name without its file extension or path. This is
+ * similar to the {@code basename} unix command. The result does not include the '{@code .}'.
+ */
+ public static String getNameWithoutExtension(Path path) {
+ Path name = path.getFileName();
+
+ // null for empty paths and root-only paths
+ if (name == null) {
+ return "";
+ }
+
+ String fileName = name.toString();
+ int dotIndex = fileName.lastIndexOf('.');
+ return dotIndex == -1 ? fileName : fileName.substring(0, dotIndex);
+ }
+
+ /**
+ * Deletes the file or directory at the given {@code path} recursively. Deletes symbolic links,
+ * not their targets (subject to the caveat below).
+ *
+ * If an I/O exception occurs attempting to read, open or delete any file under the given
+ * directory, this method skips that file and continues. All such exceptions are collected and,
+ * after attempting to delete all files, an {@code IOException} is thrown containing those
+ * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
+ *
+ * On a file system that supports symbolic links and does not support {@link
+ * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
+ * that are outside the directory being deleted. This can happen if, after checking that a
+ * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
+ * an outside directory before the call that opens the directory to read its entries.
+ *
+ * By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
+ * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
+ * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
+ *
+ * @throws NoSuchFileException if {@code path} does not exist (optional specific exception)
+ * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
+ * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
+ * specified
+ * @throws IOException if {@code path} or any file in the subtree rooted at it can't be deleted
+ * for any reason
+ */
+ public static void deleteRecursively(Path path, RecursiveDeleteOption... options)
+ throws IOException {
+ Path parentPath = getParentPath(path);
+ if (parentPath == null) {
+ throw new FileSystemException(path.toString(), null, "can't delete recursively");
+ }
+
+ Collection If an I/O exception occurs attempting to read, open or delete any file under the given
+ * directory, this method skips that file and continues. All such exceptions are collected and,
+ * after attempting to delete all files, an {@code IOException} is thrown containing those
+ * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}.
+ *
+ * On a file system that supports symbolic links and does not support {@link
+ * SecureDirectoryStream}, it is possible for a recursive delete to delete files and directories
+ * that are outside the directory being deleted. This can happen if, after checking that a
+ * file is a directory (and not a symbolic link), that directory is replaced by a symbolic link to
+ * an outside directory before the call that opens the directory to read its entries.
+ *
+ * By default, this method throws {@link InsecureRecursiveDeleteException} if it can't
+ * guarantee the security of recursive deletes. If you wish to allow the recursive deletes anyway,
+ * pass {@link RecursiveDeleteOption#ALLOW_INSECURE} to this method to override that behavior.
+ *
+ * @throws NoSuchFileException if {@code path} does not exist (optional specific exception)
+ * @throws NotDirectoryException if the file at {@code path} is not a directory (optional
+ * specific exception)
+ * @throws InsecureRecursiveDeleteException if the security of recursive deletes can't be
+ * guaranteed for the file system and {@link RecursiveDeleteOption#ALLOW_INSECURE} was not
+ * specified
+ * @throws IOException if one or more files can't be deleted for any reason
+ */
+ public static void deleteDirectoryContents(Path path, RecursiveDeleteOption... options)
+ throws IOException {
+ Collection If there is only one exception in the collection, and it is a {@link NoSuchFileException}
+ * thrown because {@code path} itself didn't exist, then throws that exception. Otherwise, the
+ * thrown exception contains all the exceptions in the given collection as suppressed exceptions.
+ */
+ private static void throwDeleteFailed(Path path, Collection Warning: On a file system that supports symbolic links, it is possible for an
+ * insecure recursive delete to delete files and directories that are outside the directory
+ * being deleted. This can happen if, after checking that a file is a directory (and not a
+ * symbolic link), that directory is deleted and replaced by a symbolic link to an outside
+ * directory before the call that opens the directory to read its entries. File systems that
+ * support {@code SecureDirectoryStream} do not have this vulnerability.
+ */
+ ALLOW_INSECURE
+}
diff --git a/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java b/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java
index c292879ba370..601ee9a8d739 100644
--- a/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java
+++ b/guava/src/com/google/common/io/InsecureRecursiveDeleteException.java
@@ -32,7 +32,7 @@
* {@link RecursiveDeleteOption#ALLOW_INSECURE} can be used to force the recursive delete method
* to proceed anyway.
*
- * @since 21.0
+ * @since 21.0 (but only since 33.4.0 in the Android flavor)
* @author Colin Decker
*/
@J2ktIncompatible
diff --git a/guava/src/com/google/common/io/MoreFiles.java b/guava/src/com/google/common/io/MoreFiles.java
index f9626022391d..4c09f58e6603 100644
--- a/guava/src/com/google/common/io/MoreFiles.java
+++ b/guava/src/com/google/common/io/MoreFiles.java
@@ -63,7 +63,7 @@
* now available via the JDK's {@link java.nio.file.Files} class for {@code Path} - check the JDK's
* class if a sibling method from {@code Files} appears to be missing from this class.
*
- * @since 21.0
+ * @since 21.0 (but only since 33.4.0 in the Android flavor)
* @author Colin Decker
*/
@J2ktIncompatible
diff --git a/guava/src/com/google/common/io/RecursiveDeleteOption.java b/guava/src/com/google/common/io/RecursiveDeleteOption.java
index a25055e40788..9b47181f0141 100644
--- a/guava/src/com/google/common/io/RecursiveDeleteOption.java
+++ b/guava/src/com/google/common/io/RecursiveDeleteOption.java
@@ -25,7 +25,7 @@
* Options for use with recursive delete methods ({@link MoreFiles#deleteRecursively} and {@link
* MoreFiles#deleteDirectoryContents}).
*
- * @since 21.0
+ * @since 21.0 (but only since 33.4.0 in the Android flavor)
* @author Colin Decker
*/
@J2ktIncompatible
+ * /
+ * 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.
+ *
+ * Warning: Security of recursive deletes
+ *
+ * Warning: Security of recursive deletes
+ *
+ *