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