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