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