diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java index 6b494ebb..945ecdb6 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java @@ -9,24 +9,40 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.net.URI; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Arrays; import java.util.Comparator; +import java.util.List; import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +//For shortening: Since filename encryption increases filename length, 50 cleartext chars are sufficient to reach the threshold public class CryptoFileSystemProviderInMemoryIntegrationTest { private static FileSystem tmpFs; @@ -58,34 +74,332 @@ public static void afterAll() throws IOException { tmpFs.close(); } - @Test - @DisplayName("Replace an existing, shortened, empty directory") - public void testReplaceExistingShortenedDirEmpty() throws IOException { + private final static String[] targetFileNamesArray = new String[]{ // + "target50Chars_56789_123456789_123456789_123456789_", // + "target15Chars__", // + "target50Chars_56789_123456789_123456789_123456.txt", // + "target15C__.txt" // + }; + + static Stream targetFileNames() { + return Arrays.stream(targetFileNamesArray); + } + + @ParameterizedTest + @MethodSource("org.cryptomator.cryptofs.CryptoFileSystemProviderInMemoryIntegrationTest#targetFileNames") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface ParameterizedFileTest { + + } + + @DisplayName("Replace an existing file") + @ParameterizedFileTest + public void testReplaceExistingFile(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var source = fs.getPath("/source.txt"); + var target = fs.getPath("/" + targetName); + Files.createFile(source); + Files.createFile(target); + + assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING)); + assertTrue(Files.notExists(source)); + assertTrue(Files.exists(target)); + } + } + + @DisplayName("Replace an existing, empty directory") + @ParameterizedFileTest + public void testReplaceExistingDirEmpty(String targetName) throws IOException { try (var fs = setupCryptoFs(50, 100, false)) { - var dirName50Chars = "/target_89_123456789_123456789_123456789_123456789_"; //since filename encryption increases filename length, 50 cleartext chars are sufficient var source = fs.getPath("/sourceDir"); - var target = fs.getPath(dirName50Chars); + var target = fs.getPath("/" + targetName); Files.createDirectory(source); Files.createDirectory(target); + assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING)); assertTrue(Files.notExists(source)); assertTrue(Files.exists(target)); } } - @Test - @DisplayName("Replace an existing, shortened file") - public void testReplaceExistingShortenedFile() throws IOException { + /* //TODO https://github.com/cryptomator/cryptofs/issues/177 + @DisplayName("Replace an existing symlink") + @ParameterizedFileTest + public void testReplaceExistingSymlink(String targetName) throws IOException { try (var fs = setupCryptoFs(50, 100, false)) { - var fiftyCharName2 = "/50char2_50char2_50char2_50char2_50char2_50char.txt"; //since filename encryption increases filename length, 50 cleartext chars are sufficient - var source = fs.getPath("/source.txt"); - var target = fs.getPath(fiftyCharName2); - Files.createFile(source); - Files.createFile(target); + var source = fs.getPath("/sourceDir"); + var linkedFromSource = fs.getPath("/linkedFromSource.txt"); + var linkedFromSourceContent = "linkedFromSourceContent!"; + + var target = fs.getPath("/" + targetName); + var linkedFromTarget = fs.getPath("/linkedFromTarget.txt"); + var linkedFromTargetContent = "linkedFromTargeContent!"; + + Files.createFile(linkedFromSource); + Files.writeString(linkedFromSource, linkedFromSourceContent, UTF_8); + Files.createFile(linkedFromTarget); + Files.writeString(linkedFromTarget, linkedFromTargetContent, UTF_8); + + Files.createSymbolicLink(source, linkedFromSource); + Files.createSymbolicLink(target, linkedFromTarget); assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING)); assertTrue(Files.notExists(source)); assertTrue(Files.exists(target)); + + //Assert linked files haven't been changed + assertTrue(Files.exists(linkedFromSource)); + assertEquals(Files.readString(linkedFromSource, UTF_8), linkedFromSourceContent); + assertFalse(Files.isSymbolicLink(linkedFromSource)); + assertTrue(Files.isRegularFile(linkedFromSource, LinkOption.NOFOLLOW_LINKS)); + + assertTrue(Files.exists(linkedFromTarget)); + assertEquals(Files.readString(linkedFromTarget, UTF_8), linkedFromTargetContent); + assertFalse(Files.isSymbolicLink(linkedFromTarget)); + assertTrue(Files.isRegularFile(linkedFromTarget, LinkOption.NOFOLLOW_LINKS)); + + //Assert link is correct + assertTrue(Files.isSymbolicLink(target)); + assertTrue(Files.isRegularFile(target /* FOLLOW_LINKS */)); + assertEquals(Files.readSymbolicLink(target), linkedFromSource); + } + }*/ + + @DisplayName("Delete not existing file") + @ParameterizedFileTest + public void testDeleteNotExisting(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var file = fs.getPath("/" + targetName); + + assertThrows(NoSuchFileException.class, () -> Files.delete(file)); + } + } + + @DisplayName("Delete regular file") + @ParameterizedFileTest + public void testDeleteFile(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var file = fs.getPath("/" + targetName); + Files.createFile(file); + + assertTrue(Files.exists(file, LinkOption.NOFOLLOW_LINKS)); + assertDoesNotThrow(() -> Files.delete(file)); + assertTrue(Files.notExists(file, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(file)); + } + } + + @DisplayName("Delete empty directory that never contained elements") + @ParameterizedFileTest + public void testDeleteDirAlwaysEmpty(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var file = fs.getPath("/" + targetName); + Files.createDirectory(file); + + assertTrue(Files.exists(file, LinkOption.NOFOLLOW_LINKS)); + assertDoesNotThrow(() -> Files.delete(file)); + assertTrue(Files.notExists(file, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(file)); + } + } + + @DisplayName("Delete directory while and after containing multiple elements") + @ParameterizedFileTest + public void testDeleteDirMultipleNagging(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var targetDir = fs.getPath("/" + targetName); + Files.createDirectory(targetDir); + + var nestedFile = targetDir.resolve("nestedFile"); + Files.createFile(nestedFile); + var nestedDir = targetDir.resolve("nestedDir"); + Files.createDirectory(nestedDir); + var nestedLink = targetDir.resolve("nestedLink"); + Files.createSymbolicLink(nestedLink, fs.getPath("linkTarget")); + + assertThrows(DirectoryNotEmptyException.class, () -> Files.delete(targetDir)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedFile, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedLink, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(nestedFile)); + assertThrows(DirectoryNotEmptyException.class, () -> Files.delete(targetDir)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedFile, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedLink, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(nestedDir)); + assertThrows(DirectoryNotEmptyException.class, () -> Files.delete(targetDir)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedLink, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(nestedLink)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedLink, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(targetDir)); + assertTrue(Files.notExists(targetDir, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(targetDir)); + } + } + + @DisplayName("Delete directory after containing multiple elements") + @ParameterizedFileTest + public void testDeleteDirMultiple(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var targetDir = fs.getPath("/" + targetName); + Files.createDirectory(targetDir); + + var nestedFile = targetDir.resolve("nestedFile"); + Files.createFile(nestedFile); + var nestedDir = targetDir.resolve("nestedDir"); + Files.createDirectory(nestedDir); + var nestedLink = targetDir.resolve("nestedLink"); + Files.createSymbolicLink(nestedLink, fs.getPath("linkTarget")); + + assertDoesNotThrow(() -> Files.delete(nestedFile)); + assertDoesNotThrow(() -> Files.delete(nestedDir)); + assertDoesNotThrow(() -> Files.delete(nestedLink)); + + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedFile, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedLink, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(targetDir)); + assertTrue(Files.notExists(targetDir, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(targetDir)); + } + } + + static Stream dirEntries() { + Stream> operations = Stream.of(Files::createFile, // + Files::createDirectory, // + nestedElement -> Files.createSymbolicLink(nestedElement, nestedElement.resolveSibling("linkTarget"))); + return operations.flatMap(elementCreator -> targetFileNames().map( // + s -> Arguments.of(s, elementCreator)) // + ); + } + + @DisplayName("Delete directory while and after containing one element") + @ParameterizedTest + @MethodSource("org.cryptomator.cryptofs.CryptoFileSystemProviderInMemoryIntegrationTest#dirEntries") + public void testDeleteDirSingleNagging(String targetName, ThrowingConsumer entryCreator) throws Throwable /* = IOE from entryCreator */ { + try (var fs = setupCryptoFs(50, 100, false)) { + var targetDir = fs.getPath("/" + targetName); + Files.createDirectory(targetDir); + + var nestedElement = targetDir.resolve("nestedElement"); + entryCreator.accept(nestedElement); + + assertThrows(DirectoryNotEmptyException.class, () -> Files.delete(targetDir)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.exists(nestedElement, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(nestedElement)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedElement, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(targetDir)); + assertTrue(Files.notExists(targetDir, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(targetDir)); + } + } + + @DisplayName("Delete directory after containing one element") + @ParameterizedTest + @MethodSource("org.cryptomator.cryptofs.CryptoFileSystemProviderInMemoryIntegrationTest#dirEntries") + public void testDeleteDirSingle(String targetName, ThrowingConsumer entryCreator) throws Throwable /* = IOE from entryCreator */ { + try (var fs = setupCryptoFs(50, 100, false)) { + var targetDir = fs.getPath("/" + targetName); + Files.createDirectory(targetDir); + + var nestedElement = targetDir.resolve("nestedElement"); + entryCreator.accept(nestedElement); + + assertDoesNotThrow(() -> Files.delete(nestedElement)); + assertTrue(Files.exists(targetDir, LinkOption.NOFOLLOW_LINKS)); + assertTrue(Files.notExists(nestedElement, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(targetDir)); + assertTrue(Files.notExists(targetDir, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(targetDir)); + } + } + + /** + * Creates the Cartesian product of {@link #targetFileNames} with itself as stream, excluding entries where both elements match. + */ + static Stream fileNamePairs() { + return targetFileNames().mapMulti((name0, intoStreamConsumer) -> { // + targetFileNames().filter(Predicate.isEqual(name0).negate()) // //Don't create pairs with the same name + .map(name1 -> Arguments.of(name0, name1)) // + .forEach(intoStreamConsumer); + }); + } + + /** + * Creates the Cartesian product of {@link #fileNamePairs} with a list of "targetCreators" as stream. + */ + static Stream linksWithCreator() { + List> operations = List.of(unused -> {}, // + Files::createFile, // + Files::createDirectory, // + nestedElement -> Files.createSymbolicLink(nestedElement, nestedElement.resolveSibling("linkTarget"))); + + return fileNamePairs().mapMulti((names, intoStreamConsumer) -> { // + operations.stream().map(operation -> { + var argValues = names.get(); + return Arguments.of(argValues[0], argValues[1], operation); + }).forEach(intoStreamConsumer); + }); + } + + @DisplayName("Delete links") + @ParameterizedTest + @MethodSource("org.cryptomator.cryptofs.CryptoFileSystemProviderInMemoryIntegrationTest#linksWithCreator") + public void testDeleteSymLink(String linkName, String targetName, ThrowingConsumer targetCreator) throws Throwable /* = IOE from entryCreator */ { + try (var fs = setupCryptoFs(50, 100, false)) { + var link = fs.getPath("/" + linkName); + var target = fs.getPath("/" + targetName); + targetCreator.accept(target); + var targetCreated = Files.exists(target, LinkOption.NOFOLLOW_LINKS); //Allow for no-op targetCreator + Files.createSymbolicLink(link, target); + + assertDoesNotThrow(() -> Files.delete(link)); + assertTrue(Files.notExists(link, LinkOption.NOFOLLOW_LINKS)); + assertThrows(NoSuchFileException.class, () -> Files.delete(link)); + + assumeTrue(targetCreated); + assertTrue(Files.exists(target, LinkOption.NOFOLLOW_LINKS)); + + assertDoesNotThrow(() -> Files.delete(target)); + assertTrue(Files.notExists(target, LinkOption.NOFOLLOW_LINKS)); + } + } + + @DisplayName("Delete directly recursive link") + @ParameterizedFileTest + public void testDeleteDirectlyRecursiveSymLink(String targetName) throws IOException { + try (var fs = setupCryptoFs(50, 100, false)) { + var link = fs.getPath("/" + targetName); + Files.createSymbolicLink(link, link); + + assertTrue(Files.exists(link, LinkOption.NOFOLLOW_LINKS)); + assertDoesNotThrow(() -> Files.delete(link)); + assertTrue(Files.notExists(link, LinkOption.NOFOLLOW_LINKS)); + + assertThrows(NoSuchFileException.class, () -> Files.delete(link)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/RegressionTest.java b/src/test/java/org/cryptomator/cryptofs/RegressionTest.java new file mode 100644 index 00000000..9c49be1b --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/RegressionTest.java @@ -0,0 +1,30 @@ +package org.cryptomator.cryptofs; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class RegressionTest { + + /** + * For cryptofs to function properly {@link Files#readAttributes(Path, Class, LinkOption...)} must throw a + * {@link NoSuchFileException} if the targeted file does not exist.
+ * This behavior is not guaranteed by the JDK specification, although the JDK itself depends on it. + * Internal discussions concluded that depending on this behavior and testing for any regressions is + * preferable to abiding strictly to the specification. + * + * @see CryptoPathMapper#getCiphertextFileType(CryptoPath) + */ + @Test + public void testNotExistingFile(@TempDir Path dir) { + assertThrows(NoSuchFileException.class, () -> Files.readAttributes(dir.resolve("notExistingFile"), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)); + assertThrows(NoSuchFileException.class, () -> Files.readAttributes(dir.resolve("notExistingFile"), BasicFileAttributes.class)); + } +}