• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.io;
18 
19 import static com.google.common.base.StandardSystemProperty.OS_NAME;
20 import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
21 import static com.google.common.jimfs.Feature.SECURE_DIRECTORY_STREAM;
22 import static com.google.common.jimfs.Feature.SYMBOLIC_LINKS;
23 import static com.google.common.truth.Truth.assertThat;
24 import static java.nio.charset.StandardCharsets.UTF_8;
25 import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
26 import static org.junit.Assert.assertThrows;
27 
28 import com.google.common.collect.ObjectArrays;
29 import com.google.common.jimfs.Configuration;
30 import com.google.common.jimfs.Feature;
31 import com.google.common.jimfs.Jimfs;
32 import java.io.IOException;
33 import java.nio.file.FileAlreadyExistsException;
34 import java.nio.file.FileSystem;
35 import java.nio.file.FileSystemException;
36 import java.nio.file.FileSystems;
37 import java.nio.file.FileVisitResult;
38 import java.nio.file.Files;
39 import java.nio.file.NoSuchFileException;
40 import java.nio.file.Path;
41 import java.nio.file.SimpleFileVisitor;
42 import java.nio.file.attribute.BasicFileAttributes;
43 import java.nio.file.attribute.FileTime;
44 import java.util.EnumSet;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Executors;
47 import java.util.concurrent.Future;
48 import junit.framework.TestCase;
49 import junit.framework.TestSuite;
50 
51 /**
52  * Tests for {@link MoreFiles}.
53  *
54  * <p>Note: {@link MoreFiles#fileTraverser()} is tested in {@link MoreFilesFileTraverserTest}.
55  *
56  * @author Colin Decker
57  */
58 
59 public class MoreFilesTest extends TestCase {
60 
suite()61   public static TestSuite suite() {
62     TestSuite suite = new TestSuite();
63     suite.addTest(
64         ByteSourceTester.tests(
65             "MoreFiles.asByteSource[Path]", SourceSinkFactories.pathByteSourceFactory(), true));
66     suite.addTest(
67         ByteSinkTester.tests(
68             "MoreFiles.asByteSink[Path]", SourceSinkFactories.pathByteSinkFactory()));
69     suite.addTest(
70         ByteSinkTester.tests(
71             "MoreFiles.asByteSink[Path, APPEND]",
72             SourceSinkFactories.appendingPathByteSinkFactory()));
73     suite.addTest(
74         CharSourceTester.tests(
75             "MoreFiles.asCharSource[Path, Charset]",
76             SourceSinkFactories.pathCharSourceFactory(),
77             false));
78     suite.addTest(
79         CharSinkTester.tests(
80             "MoreFiles.asCharSink[Path, Charset]", SourceSinkFactories.pathCharSinkFactory()));
81     suite.addTest(
82         CharSinkTester.tests(
83             "MoreFiles.asCharSink[Path, Charset, APPEND]",
84             SourceSinkFactories.appendingPathCharSinkFactory()));
85     suite.addTestSuite(MoreFilesTest.class);
86     return suite;
87   }
88 
89   private static final FileSystem FS = FileSystems.getDefault();
90 
root()91   private static Path root() {
92     return FS.getRootDirectories().iterator().next();
93   }
94 
95   private Path tempDir;
96 
97   @Override
setUp()98   protected void setUp() throws Exception {
99     tempDir = Files.createTempDirectory("MoreFilesTest");
100   }
101 
102   @Override
tearDown()103   protected void tearDown() throws Exception {
104     if (tempDir != null) {
105       // delete tempDir and its contents
106       Files.walkFileTree(
107           tempDir,
108           new SimpleFileVisitor<Path>() {
109             @Override
110             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
111                 throws IOException {
112               Files.deleteIfExists(file);
113               return FileVisitResult.CONTINUE;
114             }
115 
116             @Override
117             public FileVisitResult postVisitDirectory(Path dir, IOException exc)
118                 throws IOException {
119               if (exc != null) {
120                 return FileVisitResult.TERMINATE;
121               }
122               Files.deleteIfExists(dir);
123               return FileVisitResult.CONTINUE;
124             }
125           });
126     }
127   }
128 
createTempFile()129   private Path createTempFile() throws IOException {
130     return Files.createTempFile(tempDir, "test", ".test");
131   }
132 
testByteSource_size_ofDirectory()133   public void testByteSource_size_ofDirectory() throws IOException {
134     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
135       Path dir = fs.getPath("dir");
136       Files.createDirectory(dir);
137 
138       ByteSource source = MoreFiles.asByteSource(dir);
139 
140       assertThat(source.sizeIfKnown()).isAbsent();
141 
142       assertThrows(IOException.class, () -> source.size());
143     }
144   }
145 
testByteSource_size_ofSymlinkToDirectory()146   public void testByteSource_size_ofSymlinkToDirectory() throws IOException {
147     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
148       Path dir = fs.getPath("dir");
149       Files.createDirectory(dir);
150       Path link = fs.getPath("link");
151       Files.createSymbolicLink(link, dir);
152 
153       ByteSource source = MoreFiles.asByteSource(link);
154 
155       assertThat(source.sizeIfKnown()).isAbsent();
156 
157       assertThrows(IOException.class, () -> source.size());
158     }
159   }
160 
testByteSource_size_ofSymlinkToRegularFile()161   public void testByteSource_size_ofSymlinkToRegularFile() throws IOException {
162     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
163       Path file = fs.getPath("file");
164       Files.write(file, new byte[10]);
165       Path link = fs.getPath("link");
166       Files.createSymbolicLink(link, file);
167 
168       ByteSource source = MoreFiles.asByteSource(link);
169 
170       assertEquals(10L, (long) source.sizeIfKnown().get());
171       assertEquals(10L, source.size());
172     }
173   }
174 
testByteSource_size_ofSymlinkToRegularFile_nofollowLinks()175   public void testByteSource_size_ofSymlinkToRegularFile_nofollowLinks() throws IOException {
176     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
177       Path file = fs.getPath("file");
178       Files.write(file, new byte[10]);
179       Path link = fs.getPath("link");
180       Files.createSymbolicLink(link, file);
181 
182       ByteSource source = MoreFiles.asByteSource(link, NOFOLLOW_LINKS);
183 
184       assertThat(source.sizeIfKnown()).isAbsent();
185 
186       assertThrows(IOException.class, () -> source.size());
187     }
188   }
189 
testEqual()190   public void testEqual() throws IOException {
191     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
192       Path fooPath = fs.getPath("foo");
193       Path barPath = fs.getPath("bar");
194       MoreFiles.asCharSink(fooPath, UTF_8).write("foo");
195       MoreFiles.asCharSink(barPath, UTF_8).write("barbar");
196 
197       assertThat(MoreFiles.equal(fooPath, barPath)).isFalse();
198       assertThat(MoreFiles.equal(fooPath, fooPath)).isTrue();
199       assertThat(MoreFiles.asByteSource(fooPath).contentEquals(MoreFiles.asByteSource(fooPath)))
200           .isTrue();
201 
202       Path fooCopy = Files.copy(fooPath, fs.getPath("fooCopy"));
203       assertThat(Files.isSameFile(fooPath, fooCopy)).isFalse();
204       assertThat(MoreFiles.equal(fooPath, fooCopy)).isTrue();
205 
206       MoreFiles.asCharSink(fooCopy, UTF_8).write("boo");
207       assertThat(MoreFiles.asByteSource(fooPath).size())
208           .isEqualTo(MoreFiles.asByteSource(fooCopy).size());
209       assertThat(MoreFiles.equal(fooPath, fooCopy)).isFalse();
210 
211       // should also assert that a Path that erroneously reports a size 0 can still be compared,
212       // not sure how to do that with the Path API
213     }
214   }
215 
testEqual_links()216   public void testEqual_links() throws IOException {
217     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
218       Path fooPath = fs.getPath("foo");
219       MoreFiles.asCharSink(fooPath, UTF_8).write("foo");
220 
221       Path fooSymlink = fs.getPath("symlink");
222       Files.createSymbolicLink(fooSymlink, fooPath);
223 
224       Path fooHardlink = fs.getPath("hardlink");
225       Files.createLink(fooHardlink, fooPath);
226 
227       assertThat(MoreFiles.equal(fooPath, fooSymlink)).isTrue();
228       assertThat(MoreFiles.equal(fooPath, fooHardlink)).isTrue();
229       assertThat(MoreFiles.equal(fooSymlink, fooHardlink)).isTrue();
230     }
231   }
232 
testTouch()233   public void testTouch() throws IOException {
234     Path temp = createTempFile();
235     assertTrue(Files.exists(temp));
236     Files.delete(temp);
237     assertFalse(Files.exists(temp));
238 
239     MoreFiles.touch(temp);
240     assertTrue(Files.exists(temp));
241     MoreFiles.touch(temp);
242     assertTrue(Files.exists(temp));
243   }
244 
testTouchTime()245   public void testTouchTime() throws IOException {
246     Path temp = createTempFile();
247     assertTrue(Files.exists(temp));
248     Files.setLastModifiedTime(temp, FileTime.fromMillis(0));
249     assertEquals(0, Files.getLastModifiedTime(temp).toMillis());
250     MoreFiles.touch(temp);
251     assertThat(Files.getLastModifiedTime(temp).toMillis()).isNotEqualTo(0);
252   }
253 
testCreateParentDirectories_root()254   public void testCreateParentDirectories_root() throws IOException {
255     // We use a fake filesystem to sidestep flaky problems with Windows (b/136041958).
256     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
257       Path root = fs.getRootDirectories().iterator().next();
258       assertNull(root.getParent());
259       assertNull(root.toRealPath().getParent());
260       MoreFiles.createParentDirectories(root); // test that there's no exception
261     }
262   }
263 
testCreateParentDirectories_relativePath()264   public void testCreateParentDirectories_relativePath() throws IOException {
265     Path path = FS.getPath("nonexistent.file");
266     assertNull(path.getParent());
267     assertNotNull(path.toAbsolutePath().getParent());
268     MoreFiles.createParentDirectories(path); // test that there's no exception
269   }
270 
testCreateParentDirectories_noParentsNeeded()271   public void testCreateParentDirectories_noParentsNeeded() throws IOException {
272     Path path = tempDir.resolve("nonexistent.file");
273     assertTrue(Files.exists(path.getParent()));
274     MoreFiles.createParentDirectories(path); // test that there's no exception
275   }
276 
testCreateParentDirectories_oneParentNeeded()277   public void testCreateParentDirectories_oneParentNeeded() throws IOException {
278     Path path = tempDir.resolve("parent/nonexistent.file");
279     Path parent = path.getParent();
280     assertFalse(Files.exists(parent));
281     MoreFiles.createParentDirectories(path);
282     assertTrue(Files.exists(parent));
283   }
284 
testCreateParentDirectories_multipleParentsNeeded()285   public void testCreateParentDirectories_multipleParentsNeeded() throws IOException {
286     Path path = tempDir.resolve("grandparent/parent/nonexistent.file");
287     Path parent = path.getParent();
288     Path grandparent = parent.getParent();
289     assertFalse(Files.exists(grandparent));
290     assertFalse(Files.exists(parent));
291 
292     MoreFiles.createParentDirectories(path);
293     assertTrue(Files.exists(parent));
294     assertTrue(Files.exists(grandparent));
295   }
296 
testCreateParentDirectories_noPermission()297   public void testCreateParentDirectories_noPermission() {
298     if (isWindows()) {
299       return; // TODO: b/136041958 - Create/find a directory that we don't have permissions on?
300     }
301     Path file = root().resolve("parent/nonexistent.file");
302     Path parent = file.getParent();
303     assertFalse(Files.exists(parent));
304     assertThrows(IOException.class, () -> MoreFiles.createParentDirectories(file));
305   }
306 
testCreateParentDirectories_nonDirectoryParentExists()307   public void testCreateParentDirectories_nonDirectoryParentExists() throws IOException {
308     Path parent = createTempFile();
309     assertTrue(Files.isRegularFile(parent));
310     Path file = parent.resolve("foo");
311     assertThrows(IOException.class, () -> MoreFiles.createParentDirectories(file));
312   }
313 
testCreateParentDirectories_symlinkParentExists()314   public void testCreateParentDirectories_symlinkParentExists() throws IOException {
315     /*
316      * We use a fake filesystem to sidestep:
317      *
318      * - flaky problems with Windows (b/136041958)
319      *
320      * - the lack of support for symlinks in the default filesystem under Android's desugared
321      *   java.nio.file
322      */
323     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
324       Path symlink = fs.getPath("linkToDir");
325       Files.createSymbolicLink(symlink, fs.getRootDirectories().iterator().next());
326       Path file = symlink.resolve("foo");
327       MoreFiles.createParentDirectories(file);
328     }
329   }
330 
testGetFileExtension()331   public void testGetFileExtension() {
332     assertEquals("txt", MoreFiles.getFileExtension(FS.getPath(".txt")));
333     assertEquals("txt", MoreFiles.getFileExtension(FS.getPath("blah.txt")));
334     assertEquals("txt", MoreFiles.getFileExtension(FS.getPath("blah..txt")));
335     assertEquals("txt", MoreFiles.getFileExtension(FS.getPath(".blah.txt")));
336     assertEquals("txt", MoreFiles.getFileExtension(root().resolve("tmp/blah.txt")));
337     assertEquals("gz", MoreFiles.getFileExtension(FS.getPath("blah.tar.gz")));
338     assertEquals("", MoreFiles.getFileExtension(root()));
339     assertEquals("", MoreFiles.getFileExtension(FS.getPath(".")));
340     assertEquals("", MoreFiles.getFileExtension(FS.getPath("..")));
341     assertEquals("", MoreFiles.getFileExtension(FS.getPath("...")));
342     assertEquals("", MoreFiles.getFileExtension(FS.getPath("blah")));
343     assertEquals("", MoreFiles.getFileExtension(FS.getPath("blah.")));
344     assertEquals("", MoreFiles.getFileExtension(FS.getPath(".blah.")));
345     assertEquals("", MoreFiles.getFileExtension(root().resolve("foo.bar/blah")));
346     assertEquals("", MoreFiles.getFileExtension(root().resolve("foo/.bar/blah")));
347   }
348 
testGetNameWithoutExtension()349   public void testGetNameWithoutExtension() {
350     assertEquals("", MoreFiles.getNameWithoutExtension(FS.getPath(".txt")));
351     assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah.txt")));
352     assertEquals("blah.", MoreFiles.getNameWithoutExtension(FS.getPath("blah..txt")));
353     assertEquals(".blah", MoreFiles.getNameWithoutExtension(FS.getPath(".blah.txt")));
354     assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("tmp/blah.txt")));
355     assertEquals("blah.tar", MoreFiles.getNameWithoutExtension(FS.getPath("blah.tar.gz")));
356     assertEquals("", MoreFiles.getNameWithoutExtension(root()));
357     assertEquals("", MoreFiles.getNameWithoutExtension(FS.getPath(".")));
358     assertEquals(".", MoreFiles.getNameWithoutExtension(FS.getPath("..")));
359     assertEquals("..", MoreFiles.getNameWithoutExtension(FS.getPath("...")));
360     assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah")));
361     assertEquals("blah", MoreFiles.getNameWithoutExtension(FS.getPath("blah.")));
362     assertEquals(".blah", MoreFiles.getNameWithoutExtension(FS.getPath(".blah.")));
363     assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("foo.bar/blah")));
364     assertEquals("blah", MoreFiles.getNameWithoutExtension(root().resolve("foo/.bar/blah")));
365   }
366 
testPredicates()367   public void testPredicates() throws IOException {
368     /*
369      * We use a fake filesystem to sidestep the lack of support for symlinks in the default
370      * filesystem under Android's desugared java.nio.file.
371      */
372     try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
373       Path file = fs.getPath("file");
374       Files.createFile(file);
375       Path dir = fs.getPath("dir");
376       Files.createDirectory(dir);
377 
378       assertTrue(MoreFiles.isDirectory().apply(dir));
379       assertFalse(MoreFiles.isRegularFile().apply(dir));
380 
381       assertFalse(MoreFiles.isDirectory().apply(file));
382       assertTrue(MoreFiles.isRegularFile().apply(file));
383 
384       Path symlinkToDir = fs.getPath("symlinkToDir");
385       Path symlinkToFile = fs.getPath("symlinkToFile");
386 
387       Files.createSymbolicLink(symlinkToDir, dir);
388       Files.createSymbolicLink(symlinkToFile, file);
389 
390       assertTrue(MoreFiles.isDirectory().apply(symlinkToDir));
391       assertFalse(MoreFiles.isRegularFile().apply(symlinkToDir));
392 
393       assertFalse(MoreFiles.isDirectory().apply(symlinkToFile));
394       assertTrue(MoreFiles.isRegularFile().apply(symlinkToFile));
395 
396       assertFalse(MoreFiles.isDirectory(NOFOLLOW_LINKS).apply(symlinkToDir));
397       assertFalse(MoreFiles.isRegularFile(NOFOLLOW_LINKS).apply(symlinkToFile));
398     }
399   }
400 
401   /**
402    * Creates a new file system for testing that supports the given features in addition to
403    * supporting symbolic links. The file system is created initially having the following file
404    * structure:
405    *
406    * <pre>
407    *   /
408    *      work/
409    *         dir/
410    *            a
411    *            b/
412    *               g
413    *               h -> ../a
414    *               i/
415    *                  j/
416    *                     k
417    *                     l/
418    *            c
419    *            d -> b/i
420    *            e/
421    *            f -> /dontdelete
422    *      dontdelete/
423    *         a
424    *         b/
425    *         c
426    *      symlinktodir -> work/dir
427    * </pre>
428    */
newTestFileSystem(Feature... supportedFeatures)429   static FileSystem newTestFileSystem(Feature... supportedFeatures) throws IOException {
430     FileSystem fs =
431         Jimfs.newFileSystem(
432             Configuration.unix().toBuilder()
433                 .setSupportedFeatures(ObjectArrays.concat(SYMBOLIC_LINKS, supportedFeatures))
434                 .build());
435     Files.createDirectories(fs.getPath("dir/b/i/j/l"));
436     Files.createFile(fs.getPath("dir/a"));
437     Files.createFile(fs.getPath("dir/c"));
438     Files.createSymbolicLink(fs.getPath("dir/d"), fs.getPath("b/i"));
439     Files.createDirectory(fs.getPath("dir/e"));
440     Files.createSymbolicLink(fs.getPath("dir/f"), fs.getPath("/dontdelete"));
441     Files.createFile(fs.getPath("dir/b/g"));
442     Files.createSymbolicLink(fs.getPath("dir/b/h"), fs.getPath("../a"));
443     Files.createFile(fs.getPath("dir/b/i/j/k"));
444     Files.createDirectory(fs.getPath("/dontdelete"));
445     Files.createFile(fs.getPath("/dontdelete/a"));
446     Files.createDirectory(fs.getPath("/dontdelete/b"));
447     Files.createFile(fs.getPath("/dontdelete/c"));
448     Files.createSymbolicLink(fs.getPath("/symlinktodir"), fs.getPath("work/dir"));
449     return fs;
450   }
451 
testDirectoryDeletion_basic()452   public void testDirectoryDeletion_basic() throws IOException {
453     for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) {
454       try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
455         Path dir = fs.getPath("dir");
456         assertEquals(6, MoreFiles.listFiles(dir).size());
457 
458         method.delete(dir);
459         method.assertDeleteSucceeded(dir);
460 
461         assertEquals(
462             "contents of /dontdelete deleted by delete method " + method,
463             3,
464             MoreFiles.listFiles(fs.getPath("/dontdelete")).size());
465       }
466     }
467   }
468 
testDirectoryDeletion_emptyDir()469   public void testDirectoryDeletion_emptyDir() throws IOException {
470     for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) {
471       try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
472         Path emptyDir = fs.getPath("dir/e");
473         assertEquals(0, MoreFiles.listFiles(emptyDir).size());
474 
475         method.delete(emptyDir);
476         method.assertDeleteSucceeded(emptyDir);
477       }
478     }
479   }
480 
testDeleteRecursively_symlinkToDir()481   public void testDeleteRecursively_symlinkToDir() throws IOException {
482     try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
483       Path symlink = fs.getPath("/symlinktodir");
484       Path dir = fs.getPath("dir");
485 
486       assertEquals(6, MoreFiles.listFiles(dir).size());
487 
488       MoreFiles.deleteRecursively(symlink);
489 
490       assertFalse(Files.exists(symlink));
491       assertTrue(Files.exists(dir));
492       assertEquals(6, MoreFiles.listFiles(dir).size());
493     }
494   }
495 
testDeleteDirectoryContents_symlinkToDir()496   public void testDeleteDirectoryContents_symlinkToDir() throws IOException {
497     try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
498       Path symlink = fs.getPath("/symlinktodir");
499       Path dir = fs.getPath("dir");
500 
501       assertEquals(6, MoreFiles.listFiles(symlink).size());
502 
503       MoreFiles.deleteDirectoryContents(symlink);
504 
505       assertTrue(Files.exists(symlink, NOFOLLOW_LINKS));
506       assertTrue(Files.exists(symlink));
507       assertTrue(Files.exists(dir));
508       assertEquals(0, MoreFiles.listFiles(symlink).size());
509     }
510   }
511 
testDirectoryDeletion_sdsNotSupported_fails()512   public void testDirectoryDeletion_sdsNotSupported_fails() throws IOException {
513     for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) {
514       try (FileSystem fs = newTestFileSystem()) {
515         Path dir = fs.getPath("dir");
516         assertEquals(6, MoreFiles.listFiles(dir).size());
517 
518         assertThrows(InsecureRecursiveDeleteException.class, () -> method.delete(dir));
519 
520         assertTrue(Files.exists(dir));
521         assertEquals(6, MoreFiles.listFiles(dir).size());
522       }
523     }
524   }
525 
testDirectoryDeletion_sdsNotSupported_allowInsecure()526   public void testDirectoryDeletion_sdsNotSupported_allowInsecure() throws IOException {
527     for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) {
528       try (FileSystem fs = newTestFileSystem()) {
529         Path dir = fs.getPath("dir");
530         assertEquals(6, MoreFiles.listFiles(dir).size());
531 
532         method.delete(dir, ALLOW_INSECURE);
533         method.assertDeleteSucceeded(dir);
534 
535         assertEquals(
536             "contents of /dontdelete deleted by delete method " + method,
537             3,
538             MoreFiles.listFiles(fs.getPath("/dontdelete")).size());
539       }
540     }
541   }
542 
testDeleteRecursively_symlinkToDir_sdsNotSupported_allowInsecure()543   public void testDeleteRecursively_symlinkToDir_sdsNotSupported_allowInsecure()
544       throws IOException {
545     try (FileSystem fs = newTestFileSystem()) {
546       Path symlink = fs.getPath("/symlinktodir");
547       Path dir = fs.getPath("dir");
548 
549       assertEquals(6, MoreFiles.listFiles(dir).size());
550 
551       MoreFiles.deleteRecursively(symlink, ALLOW_INSECURE);
552 
553       assertFalse(Files.exists(symlink));
554       assertTrue(Files.exists(dir));
555       assertEquals(6, MoreFiles.listFiles(dir).size());
556     }
557   }
558 
testDeleteRecursively_nonexistingFile_throwsNoSuchFileException()559   public void testDeleteRecursively_nonexistingFile_throwsNoSuchFileException() throws IOException {
560     try (FileSystem fs = newTestFileSystem()) {
561       NoSuchFileException expected =
562           assertThrows(
563               NoSuchFileException.class,
564               () -> MoreFiles.deleteRecursively(fs.getPath("/work/nothere"), ALLOW_INSECURE));
565       assertThat(expected.getFile()).isEqualTo("/work/nothere");
566     }
567   }
568 
testDeleteDirectoryContents_symlinkToDir_sdsNotSupported_allowInsecure()569   public void testDeleteDirectoryContents_symlinkToDir_sdsNotSupported_allowInsecure()
570       throws IOException {
571     try (FileSystem fs = newTestFileSystem()) {
572       Path symlink = fs.getPath("/symlinktodir");
573       Path dir = fs.getPath("dir");
574 
575       assertEquals(6, MoreFiles.listFiles(dir).size());
576 
577       MoreFiles.deleteDirectoryContents(symlink, ALLOW_INSECURE);
578       assertEquals(0, MoreFiles.listFiles(dir).size());
579     }
580   }
581 
582   /**
583    * This test attempts to create a situation in which one thread is constantly changing a file from
584    * being a real directory to being a symlink to another directory. It then calls
585    * deleteDirectoryContents thousands of times on a directory whose subtree contains the file
586    * that's switching between directory and symlink to try to ensure that under no circumstance does
587    * deleteDirectoryContents follow the symlink to the other directory and delete that directory's
588    * contents.
589    *
590    * <p>We can only test this with a file system that supports SecureDirectoryStream, because it's
591    * not possible to protect against this if the file system doesn't.
592    */
testDirectoryDeletion_directorySymlinkRace()593   public void testDirectoryDeletion_directorySymlinkRace() throws IOException {
594     int iterations = isAndroid() ? 100 : 5000;
595     for (DirectoryDeleteMethod method : EnumSet.allOf(DirectoryDeleteMethod.class)) {
596       try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
597         Path dirToDelete = fs.getPath("dir/b/i");
598         Path changingFile = dirToDelete.resolve("j/l");
599         Path symlinkTarget = fs.getPath("/dontdelete");
600 
601         ExecutorService executor = Executors.newSingleThreadExecutor();
602         startDirectorySymlinkSwitching(changingFile, symlinkTarget, executor);
603 
604         try {
605           for (int i = 0; i < iterations; i++) {
606             try {
607               Files.createDirectories(changingFile);
608               Files.createFile(dirToDelete.resolve("j/k"));
609             } catch (FileAlreadyExistsException expected) {
610               // if a file already exists, that's fine... just continue
611             }
612 
613             try {
614               method.delete(dirToDelete);
615             } catch (FileSystemException expected) {
616               // the delete method may or may not throw an exception, but if it does that's fine
617               // and expected
618             }
619 
620             // this test is mainly checking that the contents of /dontdelete aren't deleted under
621             // any circumstances
622             assertEquals(3, MoreFiles.listFiles(symlinkTarget).size());
623 
624             Thread.yield();
625           }
626         } finally {
627           executor.shutdownNow();
628         }
629       }
630     }
631   }
632 
testDeleteRecursively_nonDirectoryFile()633   public void testDeleteRecursively_nonDirectoryFile() throws IOException {
634     try (FileSystem fs = newTestFileSystem(SECURE_DIRECTORY_STREAM)) {
635       Path file = fs.getPath("dir/a");
636       assertTrue(Files.isRegularFile(file, NOFOLLOW_LINKS));
637 
638       MoreFiles.deleteRecursively(file);
639 
640       assertFalse(Files.exists(file, NOFOLLOW_LINKS));
641 
642       Path symlink = fs.getPath("/symlinktodir");
643       assertTrue(Files.isSymbolicLink(symlink));
644 
645       Path realSymlinkTarget = symlink.toRealPath();
646       assertTrue(Files.isDirectory(realSymlinkTarget, NOFOLLOW_LINKS));
647 
648       MoreFiles.deleteRecursively(symlink);
649 
650       assertFalse(Files.exists(symlink, NOFOLLOW_LINKS));
651       assertTrue(Files.isDirectory(realSymlinkTarget, NOFOLLOW_LINKS));
652     }
653   }
654 
655   /**
656    * Starts a new task on the given executor that switches (deletes and replaces) a file between
657    * being a directory and being a symlink. The given {@code file} is the file that should switch
658    * between being a directory and being a symlink, while the given {@code target} is the target the
659    * symlink should have.
660    */
startDirectorySymlinkSwitching( final Path file, final Path target, ExecutorService executor)661   private static void startDirectorySymlinkSwitching(
662       final Path file, final Path target, ExecutorService executor) {
663     @SuppressWarnings("unused") // https://errorprone.info/bugpattern/FutureReturnValueIgnored
664     Future<?> possiblyIgnoredError =
665         executor.submit(
666             new Runnable() {
667               @Override
668               public void run() {
669                 boolean createSymlink = false;
670                 while (!Thread.interrupted()) {
671                   try {
672                     // trying to switch between a real directory and a symlink (dir -> /a)
673                     if (Files.deleteIfExists(file)) {
674                       if (createSymlink) {
675                         Files.createSymbolicLink(file, target);
676                       } else {
677                         Files.createDirectory(file);
678                       }
679                       createSymlink = !createSymlink;
680                     }
681                   } catch (IOException tolerated) {
682                     // it's expected that some of these will fail
683                   }
684 
685                   Thread.yield();
686                 }
687               }
688             });
689   }
690 
691   /** Enum defining the two MoreFiles methods that delete directory contents. */
692   private enum DirectoryDeleteMethod {
693     DELETE_DIRECTORY_CONTENTS {
694       @Override
delete(Path path, RecursiveDeleteOption... options)695       public void delete(Path path, RecursiveDeleteOption... options) throws IOException {
696         MoreFiles.deleteDirectoryContents(path, options);
697       }
698 
699       @Override
assertDeleteSucceeded(Path path)700       public void assertDeleteSucceeded(Path path) throws IOException {
701         assertEquals(
702             "contents of directory " + path + " not deleted with delete method " + this,
703             0,
704             MoreFiles.listFiles(path).size());
705       }
706     },
707     DELETE_RECURSIVELY {
708       @Override
delete(Path path, RecursiveDeleteOption... options)709       public void delete(Path path, RecursiveDeleteOption... options) throws IOException {
710         MoreFiles.deleteRecursively(path, options);
711       }
712 
713       @Override
assertDeleteSucceeded(Path path)714       public void assertDeleteSucceeded(Path path) throws IOException {
715         assertFalse("file " + path + " not deleted with delete method " + this, Files.exists(path));
716       }
717     };
718 
delete(Path path, RecursiveDeleteOption... options)719     public abstract void delete(Path path, RecursiveDeleteOption... options) throws IOException;
720 
assertDeleteSucceeded(Path path)721     public abstract void assertDeleteSucceeded(Path path) throws IOException;
722   }
723 
isWindows()724   private static boolean isWindows() {
725     return OS_NAME.value().startsWith("Windows");
726   }
727 
isAndroid()728   private static boolean isAndroid() {
729     return System.getProperty("java.runtime.name", "").contains("Android");
730   }
731 }
732