diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 01633d38..4b0b1d85 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -13,6 +13,8 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.Optional; +import java.util.function.Consumer; import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; @@ -41,18 +44,20 @@ public class CryptoPathMapper { private final DirectoryIdProvider dirIdProvider; private final LongFileNameProvider longFileNameProvider; private final VaultConfig vaultConfig; + private final Consumer eventConsumer; private final LoadingCache ciphertextNames; private final CiphertextDirCache ciphertextDirCache; private final CiphertextDirectory rootDirectory; @Inject - CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, VaultConfig vaultConfig) { + CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, VaultConfig vaultConfig, Consumer eventConsumer) { this.dataRoot = pathToVault.resolve(DATA_DIR_NAME); this.cryptor = cryptor; this.dirIdProvider = dirIdProvider; this.longFileNameProvider = longFileNameProvider; this.vaultConfig = vaultConfig; + this.eventConsumer = eventConsumer; this.ciphertextNames = Caffeine.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(this::getCiphertextFileName); this.ciphertextDirCache = new CiphertextDirCache(); this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID); @@ -98,6 +103,7 @@ public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws } else if (ciphertextPath.isShortened() && Files.exists(ciphertextPath.getFilePath(), LinkOption.NOFOLLOW_LINKS)) { return CiphertextFileType.FILE; } else { + eventConsumer.accept(new BrokenFileNodeEvent(cleartextPath, ciphertextPath.getRawPath())); LOG.warn("Did not find valid content inside of {}", ciphertextPath.getRawPath()); throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath.getRawPath()); } @@ -111,7 +117,7 @@ public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { CryptoPath parentPath = cleartextPath.getParent(); if (parentPath == null) { - throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath); + throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath); //TODO: cleartext path in Logs! } CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryIdLoader.java b/src/main/java/org/cryptomator/cryptofs/DirectoryIdLoader.java index 873fda96..197c6b20 100644 --- a/src/main/java/org/cryptomator/cryptofs/DirectoryIdLoader.java +++ b/src/main/java/org/cryptomator/cryptofs/DirectoryIdLoader.java @@ -1,11 +1,12 @@ package org.cryptomator.cryptofs; import com.github.benmanes.caffeine.cache.CacheLoader; +import org.cryptomator.cryptofs.event.BrokenDirFileEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; import javax.inject.Inject; import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; @@ -13,14 +14,18 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.UUID; +import java.util.function.Consumer; @CryptoFileSystemScoped class DirectoryIdLoader implements CacheLoader { private static final int MAX_DIR_ID_LENGTH = 1000; + private final Consumer eventConsumer; + @Inject - public DirectoryIdLoader() { + public DirectoryIdLoader(Consumer eventConsumer) { + this.eventConsumer = eventConsumer; } @Override @@ -29,8 +34,10 @@ public String load(Path dirFilePath) throws IOException { InputStream in = Channels.newInputStream(ch)) { long size = ch.size(); if (size == 0) { + eventConsumer.accept(new BrokenDirFileEvent(dirFilePath)); throw new IOException("Invalid, empty directory file: " + dirFilePath); } else if (size > MAX_DIR_ID_LENGTH) { + eventConsumer.accept(new BrokenDirFileEvent(dirFilePath)); throw new IOException("Unexpectedly large directory file: " + dirFilePath); } else { assert size <= MAX_DIR_ID_LENGTH; // thus int diff --git a/src/main/java/org/cryptomator/cryptofs/event/BrokenDirFileEvent.java b/src/main/java/org/cryptomator/cryptofs/event/BrokenDirFileEvent.java new file mode 100644 index 00000000..15882d60 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/event/BrokenDirFileEvent.java @@ -0,0 +1,12 @@ +package org.cryptomator.cryptofs.event; + +import java.nio.file.Path; + +/** + * Emitted, if a dir.c9r file is empty or exceeds 1000 Bytes. + * + * @param ciphertextPath path to the broken dir.c9r file + */ +public record BrokenDirFileEvent(Path ciphertextPath) implements FilesystemEvent { + +} diff --git a/src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java b/src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java new file mode 100644 index 00000000..6c9d7cac --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/event/BrokenFileNodeEvent.java @@ -0,0 +1,15 @@ +package org.cryptomator.cryptofs.event; + +import java.nio.file.Path; + +/** + * Emitted, if a path within the cryptographic filesystem is accessed, but the directory representing it is missing identification files. + * + * @param cleartextPath path within the cryptographic filesystem + * @param ciphertextPath path of the incomplete, encrypted directory + * + * @see org.cryptomator.cryptofs.health.type.UnknownType + */ +public record BrokenFileNodeEvent(Path cleartextPath, Path ciphertextPath) implements FilesystemEvent { + +} diff --git a/src/main/java/org/cryptomator/cryptofs/event/DecryptionFailedEvent.java b/src/main/java/org/cryptomator/cryptofs/event/DecryptionFailedEvent.java index d7b9eba7..c9674552 100644 --- a/src/main/java/org/cryptomator/cryptofs/event/DecryptionFailedEvent.java +++ b/src/main/java/org/cryptomator/cryptofs/event/DecryptionFailedEvent.java @@ -1,7 +1,5 @@ package org.cryptomator.cryptofs.event; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; - import java.nio.file.Path; /** @@ -10,6 +8,5 @@ * @param ciphertextPath path to the encrypted resource * @param e thrown exception */ -public record DecryptionFailedEvent(Path ciphertextPath, AuthenticationFailedException e) implements FilesystemEvent { - +public record DecryptionFailedEvent(Path ciphertextPath, Exception e) implements FilesystemEvent { } diff --git a/src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java b/src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java index 2a80d3b2..dcd5f8f0 100644 --- a/src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java +++ b/src/main/java/org/cryptomator/cryptofs/event/FilesystemEvent.java @@ -22,6 +22,6 @@ * * @apiNote Events might have occured a long time ago in a galaxy far, far away... therefore, any feedback method is non-blocking and might fail due to changes in the filesystem. */ -public sealed interface FilesystemEvent permits ConflictResolutionFailedEvent, ConflictResolvedEvent, DecryptionFailedEvent { +public sealed interface FilesystemEvent permits BrokenDirFileEvent, BrokenFileNodeEvent, ConflictResolutionFailedEvent, ConflictResolvedEvent, DecryptionFailedEvent { } diff --git a/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java b/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java index 0ca5ad5d..438c5a4e 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java @@ -2,7 +2,6 @@ import org.cryptomator.cryptofs.event.DecryptionFailedEvent; import org.cryptomator.cryptofs.event.FilesystemEvent; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; @@ -81,9 +80,7 @@ FileHeader loadExisting(FileChannel ch) throws IOException { isPersisted.set(true); return existingHeader; } catch (IllegalArgumentException | CryptoException e) { - if (e instanceof AuthenticationFailedException afe) { - eventConsumer.accept(new DecryptionFailedEvent(path.get(), afe)); - } + eventConsumer.accept(new DecryptionFailedEvent(path.get(), e)); throw new IOException("Unable to decrypt header of file " + path.get(), e); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 93cdda8d..8841a01c 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -10,6 +10,8 @@ import com.google.common.base.Strings; import org.cryptomator.cryptofs.common.CiphertextFileType; +import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; @@ -17,6 +19,9 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -27,9 +32,13 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + public class CryptoPathMapperTest { private final Path pathToVault = Mockito.mock(Path.class, "pathToVault"); @@ -41,6 +50,7 @@ public class CryptoPathMapperTest { private final VaultConfig vaultConfig = Mockito.mock(VaultConfig.class); private final Symlinks symlinks = Mockito.mock(Symlinks.class); private final CryptoFileSystemImpl fileSystem = Mockito.mock(CryptoFileSystemImpl.class); + private final Consumer eventConsumer = mock(Consumer.class); @BeforeEach public void setup() { @@ -74,7 +84,7 @@ public void testPathEncryptionForRoot() throws IOException { Path d0000 = Mockito.mock(Path.class); Mockito.when(d00.resolve("00")).thenReturn(d0000); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path(); Assertions.assertEquals(d0000, path); } @@ -98,7 +108,7 @@ public void testPathEncryptionForFoo() throws IOException { Path d0001 = Mockito.mock(Path.class); Mockito.when(d00.resolve("01")).thenReturn(d0001); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path(); Assertions.assertEquals(d0001, path); } @@ -132,7 +142,7 @@ public void testPathEncryptionForFooBar() throws IOException { Path d0002 = Mockito.mock(Path.class); Mockito.when(d00.resolve("02")).thenReturn(d0002); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path(); Assertions.assertEquals(d0002, path); } @@ -170,7 +180,7 @@ public void testPathEncryptionDEEP() throws IOException { var testPath = "/" + IntStream.range(0, maxNestingDepth).mapToObj("%02d"::formatted).collect(Collectors.joining("/")) + "/cleartextFile"; - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); Path path = mapper.getCiphertextFilePath(fileSystem.getPath(testPath)).getRawPath(); Assertions.assertEquals(finalEncryptedFile, path); } @@ -207,7 +217,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab); Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("baz"), Mockito.any())).thenReturn("zab"); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")).getRawPath(); Assertions.assertEquals(d0002zab, path); } @@ -255,7 +265,7 @@ public void setup() throws IOException { @Test public void testGetCiphertextFileTypeOfRootPath() throws IOException { - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CiphertextFileType type = mapper.getCiphertextFileType(fileSystem.getRootPath()); Assertions.assertEquals(CiphertextFileType.DIRECTORY, type); } @@ -264,7 +274,7 @@ public void testGetCiphertextFileTypeOfRootPath() throws IOException { public void testGetCiphertextFileTypeForNonexistingFile() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CryptoPath path = fileSystem.getPath("/CLEAR"); Assertions.assertThrows(NoSuchFileException.class, () -> { @@ -277,7 +287,7 @@ public void testGetCiphertextFileTypeForFile() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); Mockito.when(c9rAttrs.isDirectory()).thenReturn(false); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -293,7 +303,7 @@ public void testGetCiphertextFileTypeForDirectory() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); Mockito.when(underlyingFileSystemProvider.exists(dirFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(true); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -309,7 +319,7 @@ public void testGetCiphertextFileTypeForSymlink() throws IOException { Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); Mockito.when(underlyingFileSystemProvider.exists(symlinkFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(true); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CryptoPath path = fileSystem.getPath("/CLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); @@ -327,13 +337,78 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException { Mockito.when(longFileNameProvider.deflate(Mockito.any())).thenReturn(new LongFileNameProvider.DeflatedFileName(c9rPath, null, null)); Mockito.when(underlyingFileSystemProvider.exists(contentsFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(true); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); CryptoPath path = fileSystem.getPath("/LONGCLEAR"); CiphertextFileType type = mapper.getCiphertextFileType(path); Assertions.assertEquals(CiphertextFileType.FILE, type); } + @DisplayName("Test ciphertextFileType detection priority") + @ParameterizedTest + @CsvSource(value = {"true, true, true", "true, false, true", "true, true, false", "false, true, true"}) + public void testDetectionPriority(boolean dirFileExists, boolean symlinkFileExists, boolean contentsFileExists) throws IOException { + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); + Mockito.when(c9rAttrs.isDirectory()).thenReturn(true); + var dirFileAttr = Mockito.mock(BasicFileAttributes.class); + var symlinkFileAttr = Mockito.mock(BasicFileAttributes.class); + var contentsFileAttr = Mockito.mock(BasicFileAttributes.class); + if (dirFileExists) { + Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(dirFileAttr); + } else { + Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + } + if (symlinkFileExists) { + Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(symlinkFileAttr); + } else { + Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + } + if (contentsFileExists) { + Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(contentsFileAttr); + } else { + Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + } + + CiphertextFileType expectedType; + if (dirFileExists) { + expectedType = CiphertextFileType.DIRECTORY; + } else if (symlinkFileExists) { + expectedType = CiphertextFileType.SYMLINK; + } else { + expectedType = CiphertextFileType.FILE; + } + + Mockito.when(underlyingFileSystemProvider.exists(dirFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(dirFileExists); + Mockito.when(underlyingFileSystemProvider.exists(symlinkFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(symlinkFileExists); + Mockito.when(underlyingFileSystemProvider.exists(contentsFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(contentsFileExists); + + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); + + CryptoPath path = fileSystem.getPath("/CLEAR"); + CiphertextFileType type = mapper.getCiphertextFileType(path); + Assertions.assertEquals(expectedType, type); + } + + @Test + @DisplayName("Throw NoSuchFileException if no known file exists") + public void testNoKnownFileExists() throws IOException { + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); + Mockito.when(c9rAttrs.isDirectory()).thenReturn(true); + Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.exists(dirFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(false); + Mockito.when(underlyingFileSystemProvider.exists(symlinkFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(false); + Mockito.when(underlyingFileSystemProvider.exists(contentsFilePath, LinkOption.NOFOLLOW_LINKS)).thenReturn(false); + + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); + + CryptoPath path = fileSystem.getPath("/CLEAR"); + Assertions.assertThrows(NoSuchFileException.class, () -> mapper.getCiphertextFileType(path)); + var isBrokenFileNodeEvent = (ArgumentMatcher) ev -> ev instanceof BrokenFileNodeEvent; + verify(eventConsumer).accept(ArgumentMatchers.argThat(isBrokenFileNodeEvent)); + } + } @Nested @@ -386,7 +461,7 @@ void beforeEach() throws IOException { @Test @DisplayName("Invalidating node causes cache miss on next retrieval") public void testRemovedEntryMiss() throws IOException { - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); var fooPath = fileSystem.getPath("/foo"); mapper.getCiphertextDir(fooPath); mapper.invalidatePathMapping(fooPath); @@ -401,7 +476,7 @@ public void testRemovedEntryChildMiss() throws IOException { var fooPath = fileSystem.getPath("/foo"); var fooBarPath = fileSystem.getPath("/foo/bar"); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); mapper.getCiphertextDir(fooPath); mapper.getCiphertextDir(fooBarPath); mapper.invalidatePathMapping(fooPath); @@ -417,7 +492,7 @@ public void testMoveEntryOldMissNewHit() throws IOException { var fooPath = fileSystem.getPath("/foo"); var kikPath = fileSystem.getPath("/kik"); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); mapper.getCiphertextDir(fooPath); mapper.movePathMapping(fooPath, kikPath); var mapperSpy = Mockito.spy(mapper); @@ -433,7 +508,7 @@ public void testMoveEntryOldChildMiss() throws IOException { var fooBarPath = fileSystem.getPath("/foo/bar"); var kikPath = fileSystem.getPath("/kik"); - CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig, eventConsumer); mapper.getCiphertextDir(fooPath); mapper.getCiphertextDir(fooBarPath); mapper.movePathMapping(fooPath, kikPath); diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java index 37171a74..0d537bbc 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java @@ -1,26 +1,32 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.event.BrokenDirFileEvent; +import org.cryptomator.cryptofs.event.FilesystemEvent; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.FileSystem; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; +import java.util.function.Consumer; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class DirectoryIdLoaderTest { @@ -29,14 +35,16 @@ public class DirectoryIdLoaderTest { private final FileSystem fileSystem = mock(FileSystem.class); private final Path dirFilePath = mock(Path.class); private final Path otherDirFilePath = mock(Path.class); + private final Consumer eventConsumer = mock(Consumer.class); - private final DirectoryIdLoader inTest = new DirectoryIdLoader(); + private final DirectoryIdLoader inTest = new DirectoryIdLoader(eventConsumer); @BeforeEach public void setup() { when(dirFilePath.getFileSystem()).thenReturn(fileSystem); when(otherDirFilePath.getFileSystem()).thenReturn(fileSystem); when(fileSystem.provider()).thenReturn(provider); + doNothing().when(eventConsumer).accept(any()); } @Test @@ -88,6 +96,8 @@ public void testIOExceptionWhenExistingFileIsEmpty() throws IOException { inTest.load(dirFilePath); }); MatcherAssert.assertThat(ioException.getMessage(), containsString("Invalid, empty directory file")); + var isBrokenDirFileEvent = (ArgumentMatcher) ev -> ev instanceof BrokenDirFileEvent; + verify(eventConsumer).accept(ArgumentMatchers.argThat(isBrokenDirFileEvent)); } @Test @@ -100,6 +110,8 @@ public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException { inTest.load(dirFilePath); }); MatcherAssert.assertThat(ioException.getMessage(), containsString("Unexpectedly large directory file")); + var isBrokenDirFileEvent = (ArgumentMatcher) ev -> ev instanceof BrokenDirFileEvent; + verify(eventConsumer).accept(ArgumentMatchers.argThat(isBrokenDirFileEvent)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java index afc8ab31..8b3acfeb 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java @@ -12,6 +12,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -25,7 +27,6 @@ import java.util.function.Consumer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -89,26 +90,17 @@ public void testLoadExisting() throws IOException, AuthenticationFailedException Assertions.assertTrue(inTest.headerIsPersisted().get()); } - @Test + @ParameterizedTest(name = "Failures in decryption") @DisplayName("load failure due to authenticationFailedException") - public void testLoadExistingFailureWithAuthFailed() { - Mockito.doThrow(AuthenticationFailedException.class).when(fileHeaderCryptor).decryptHeader(Mockito.any()); + @ValueSource(classes = {AuthenticationFailedException.class, IllegalArgumentException.class}) + public void testLoadExistingFailure(Class clazz) { + Mockito.doThrow(clazz).when(fileHeaderCryptor).decryptHeader(Mockito.any()); Assertions.assertThrows(IOException.class, () -> inTest.loadExisting(channel)); var isDecryptionFailedEvent = (ArgumentMatcher) ev -> ev instanceof DecryptionFailedEvent; verify(eventConsumer).accept(ArgumentMatchers.argThat(isDecryptionFailedEvent)); } - @Test - @DisplayName("load failure due to IllegalArgumentException") - public void testLoadExistingFailureWithIllegalArgument() { - Mockito.doThrow(IllegalArgumentException.class).when(fileHeaderCryptor).decryptHeader(Mockito.any()); - - Assertions.assertThrows(IOException.class, () -> inTest.loadExisting(channel)); - var isDecryptionFailedEvent = (ArgumentMatcher) ev -> ev instanceof DecryptionFailedEvent; - verify(eventConsumer, never()).accept(ArgumentMatchers.argThat(isDecryptionFailedEvent)); - } - } @Nested