diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java
index 7f82e2d62..de268656b 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystem.java
@@ -1,5 +1,7 @@
package org.cryptomator.cryptofs;
+import org.cryptomator.cryptofs.common.Constants;
+
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
@@ -41,6 +43,26 @@ public abstract class CryptoFileSystem extends FileSystem {
*/
public abstract Path getCiphertextPath(Path cleartextPath) throws IOException;
+ /**
+ * Computes from a valid,encrypted node (file or folder) its cleartext name.
+ *
+ * Due to the structure of a vault, an encrypted node is valid if:
+ *
+ * - the path points into the vault (duh!)
+ * - the "file" extension is {@value Constants#CRYPTOMATOR_FILE_SUFFIX} or {@value Constants#DEFLATED_FILE_SUFFIX}
+ * - the node name is at least {@value Constants#MIN_CIPHER_NAME_LENGTH} characters long
+ * - it is located at depth 4 from the vault storage root, i.e. d/AB/CDEFG...XYZ/validFile.c9r
+ *
+ *
+ * @param ciphertextNode path to the ciphertext file or directory
+ * @return the cleartext name of the ciphertext file or directory
+ * @throws java.nio.file.NoSuchFileException if the ciphertextFile does not exist
+ * @throws IOException if an I/O error occurs reading the ciphertext files
+ * @throws IllegalArgumentException if {@param ciphertextNode} is not a valid ciphertext content node of the vault
+ * @throws UnsupportedOperationException if the directory containing the {@param ciphertextNode} does not have a {@value Constants#DIR_ID_BACKUP_FILE_NAME} file
+ */
+ public abstract String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException;
+
/**
* Provides file system performance statistics.
*
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index 766cdd9cc..905e508d3 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -17,8 +17,8 @@
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
import org.cryptomator.cryptofs.common.FinallyUtil;
import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter;
-import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
import org.cryptomator.cryptofs.dir.DirectoryStreamFactory;
+import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
import org.cryptomator.cryptolib.api.Cryptor;
@@ -95,16 +95,17 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
private final CryptoPath rootPath;
private final CryptoPath emptyPath;
+ private final FileNameDecryptor fileNameDecryptor;
private volatile boolean open = true;
@Inject
- public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor,
- CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory,
- PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup,
- AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
- OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag,
- CryptoFileSystemProperties fileSystemProperties) {
+ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor, //
+ CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory, //
+ PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, DirectoryIdBackup dirIdBackup, //
+ AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, //
+ OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, //
+ CryptoFileSystemProperties fileSystemProperties, FileNameDecryptor fileNameDecryptor) {
this.provider = provider;
this.cryptoFileSystems = cryptoFileSystems;
this.pathToVault = pathToVault;
@@ -129,6 +130,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
this.rootPath = cryptoPathFactory.rootFor(this);
this.emptyPath = cryptoPathFactory.emptyFor(this);
+ this.fileNameDecryptor = fileNameDecryptor;
}
@Override
@@ -151,6 +153,11 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException {
}
}
+ @Override
+ public String getCleartextName(Path ciphertextNode) throws IOException, UnsupportedOperationException {
+ return fileNameDecryptor.decryptFilename(ciphertextNode);
+ }
+
@Override
public CryptoFileSystemStats getStats() {
return stats;
diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java b/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java
new file mode 100644
index 000000000..ff312c8af
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/FileNameDecryptor.java
@@ -0,0 +1,99 @@
+package org.cryptomator.cryptofs;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.common.StringUtils;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.FileSystemException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+/**
+ * @see CryptoFileSystem#getCleartextName(Path)
+ */
+@CryptoFileSystemScoped
+class FileNameDecryptor {
+
+ private final DirectoryIdBackup dirIdBackup;
+ private final LongFileNameProvider longFileNameProvider;
+ private final Path vaultPath;
+ private final FileNameCryptor fileNameCryptor;
+
+ @Inject
+ public FileNameDecryptor(@PathToVault Path vaultPath, Cryptor cryptor, DirectoryIdBackup dirIdBackup, LongFileNameProvider longFileNameProvider) {
+ this.vaultPath = vaultPath;
+ this.fileNameCryptor = cryptor.fileNameCryptor();
+ this.dirIdBackup = dirIdBackup;
+ this.longFileNameProvider = longFileNameProvider;
+ }
+
+ public String decryptFilename(Path ciphertextNode) throws IOException, UnsupportedOperationException {
+ validatePath(ciphertextNode.toAbsolutePath());
+ return decryptFilenameInternal(ciphertextNode);
+ }
+
+ @VisibleForTesting
+ String decryptFilenameInternal(Path ciphertextNode) throws IOException, UnsupportedOperationException {
+ byte[] dirId = null;
+ try {
+ dirId = dirIdBackup.read(ciphertextNode);
+ } catch (NoSuchFileException e) {
+ throw new UnsupportedOperationException("Directory does not have a " + Constants.DIR_ID_BACKUP_FILE_NAME + " file.");
+ } catch (CryptoException | IllegalStateException e) {
+ throw new FileSystemException(ciphertextNode.toString(), null, "Decryption of dirId backup file failed:" + e);
+ }
+ var fullCipherNodeName = ciphertextNode.getFileName().toString();
+ var cipherNodeExtension = fullCipherNodeName.substring(fullCipherNodeName.length() - 4);
+
+ String actualEncryptedName = switch (cipherNodeExtension) {
+ case Constants.CRYPTOMATOR_FILE_SUFFIX -> StringUtils.removeEnd(fullCipherNodeName, Constants.CRYPTOMATOR_FILE_SUFFIX);
+ case Constants.DEFLATED_FILE_SUFFIX -> longFileNameProvider.inflate(ciphertextNode);
+ default -> throw new IllegalStateException("SHOULD NOT REACH HERE");
+ };
+ try {
+ return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), actualEncryptedName, dirId);
+ } catch (CryptoException e) {
+ throw new FileSystemException(ciphertextNode.toString(), null, "Filname decryption failed:" + e);
+ }
+ }
+
+ @VisibleForTesting
+ void validatePath(Path absolutePath) {
+ if (!belongsToVault(absolutePath)) {
+ throw new IllegalArgumentException("Node %s is not a part of vault %s".formatted(absolutePath, vaultPath));
+ }
+ if (!isAtCipherNodeLevel(absolutePath)) {
+ throw new IllegalArgumentException("Node %s is not located at depth 4 from vault storage root".formatted(absolutePath));
+ }
+ if (!(hasCipherNodeExtension(absolutePath) && hasMinimumFileNameLength(absolutePath))) {
+ throw new IllegalArgumentException("Node %s does not end with %s or %s or filename is shorter than %d characters.".formatted(absolutePath, Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX, Constants.MIN_CIPHER_NAME_LENGTH));
+ }
+ }
+
+ boolean hasCipherNodeExtension(Path p) {
+ var name = p.getFileName();
+ return name != null && Stream.of(Constants.CRYPTOMATOR_FILE_SUFFIX, Constants.DEFLATED_FILE_SUFFIX).anyMatch(name.toString()::endsWith);
+ }
+
+ boolean isAtCipherNodeLevel(Path absolutPah) {
+ if (!absolutPah.isAbsolute()) {
+ throw new IllegalArgumentException("Path " + absolutPah + "must be absolute");
+ }
+ return absolutPah.subpath(vaultPath.getNameCount(), absolutPah.getNameCount()).getNameCount() == 4;
+ }
+
+ boolean hasMinimumFileNameLength(Path p) {
+ return p.getFileName().toString().length() >= Constants.MIN_CIPHER_NAME_LENGTH;
+ }
+
+ boolean belongsToVault(Path p) {
+ return p.startsWith(vaultPath);
+ }
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 43e9e2cef..5343dfc7c 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -105,6 +105,7 @@ public class CryptoFileSystemImplTest {
private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class);
private final ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class);
private final CryptoFileSystemProperties fileSystemProperties = mock(CryptoFileSystemProperties.class);
+ private final FileNameDecryptor filenameDecryptor = mock(FileNameDecryptor.class);
private final CryptoPath root = mock(CryptoPath.class);
private final CryptoPath empty = mock(CryptoPath.class);
@@ -127,7 +128,7 @@ public void setup() {
pathMatcherFactory, directoryStreamFactory, dirIdProvider, dirIdBackup, //
fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider, //
openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag, //
- fileSystemProperties);
+ fileSystemProperties, filenameDecryptor);
}
@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
index cd0d16794..fb120e4b8 100644
--- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java
@@ -1,6 +1,8 @@
package org.cryptomator.cryptofs;
import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.health.dirid.OrphanContentDirTest;
+import org.cryptomator.cryptofs.util.TestCryptoException;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
@@ -103,17 +105,13 @@ public void contentLongerThan36Chars() throws IOException {
@DisplayName("If the backup file cannot be decrypted, a CryptoException is thrown")
public void invalidEncryptionThrowsCryptoException() throws IOException {
var dirIdBackupSpy = spy(dirIdBackup);
- var expectedException = new MyCryptoException();
+ var expectedException = new TestCryptoException();
Mockito.when(dirIdBackupSpy.wrapDecryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(decChannel);
Mockito.when(decChannel.read(Mockito.any())).thenThrow(expectedException);
var actual = Assertions.assertThrows(CryptoException.class, () -> dirIdBackupSpy.read(contentPath));
Assertions.assertEquals(expectedException, actual);
}
- static class MyCryptoException extends CryptoException {
-
- }
-
@Test
@DisplayName("IOException accessing the file is rethrown")
public void ioException() throws IOException {
diff --git a/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java
new file mode 100644
index 000000000..c5874d8bc
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/FileNameDecryptorTest.java
@@ -0,0 +1,209 @@
+package org.cryptomator.cryptofs;
+
+import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.util.TestCryptoException;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.nio.file.FileSystemException;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FileNameDecryptorTest {
+
+ @TempDir
+ Path tmpPath;
+ Path vaultPath = mock(Path.class);
+ DirectoryIdBackup dirIdBackup = mock(DirectoryIdBackup.class);
+ LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class);
+ FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class);
+ FileNameDecryptor testObj;
+ FileNameDecryptor testObjSpy;
+
+ @BeforeEach
+ public void beforeEach() {
+ var cryptor = mock(Cryptor.class);
+ when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
+ testObj = new FileNameDecryptor(vaultPath, cryptor, dirIdBackup, longFileNameProvider);
+ testObjSpy = Mockito.spy(testObj);
+ }
+
+ @ParameterizedTest
+ @DisplayName("Given a ciphertextNode, it's clearname is returned")
+ @ValueSource(strings = {Constants.DEFLATED_FILE_SUFFIX, Constants.CRYPTOMATOR_FILE_SUFFIX})
+ public void success(String fileExtension) throws IOException {
+ var ciphertextNodeNameName = "someFile";
+ var ciphertextNode = tmpPath.resolve(ciphertextNodeNameName + fileExtension);
+ var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'};
+ var expectedClearName = "veryClearText";
+ when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId);
+ when(longFileNameProvider.inflate(ciphertextNode)).thenReturn(ciphertextNodeNameName);
+ when(fileNameCryptor.decryptFilename(any(), eq(ciphertextNodeNameName), eq(dirId))).thenReturn(expectedClearName);
+
+ var result = testObjSpy.decryptFilenameInternal(ciphertextNode);
+ verify(fileNameCryptor).decryptFilename(any(), eq(ciphertextNodeNameName), eq(dirId));
+ Assertions.assertEquals(expectedClearName, result);
+ }
+
+ @Test
+ @DisplayName("Path is validated before computation")
+ public void validatePath() throws IOException {
+ var ciphertextNode = tmpPath.resolve("someFile.c9r");
+ Mockito.doNothing().when(testObjSpy).validatePath(any());
+ Mockito.doReturn("veryClearName").when(testObjSpy).decryptFilenameInternal(any());
+
+ var actual = testObjSpy.decryptFilename(ciphertextNode);
+ Assertions.assertEquals("veryClearName", actual);
+ }
+
+ @Test
+ @DisplayName("If the dirId backup file does not exists, throw UnsupportedOperationException")
+ public void notExistingDirIdFile() throws IOException {
+ var ciphertextNode = tmpPath.resolve("toDecrypt.c9r");
+ when(dirIdBackup.read(ciphertextNode)).thenThrow(NoSuchFileException.class);
+
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode));
+ }
+
+ @Test
+ @DisplayName("If the dirId cannot be read, throw FileSystemException")
+ public void notReadableDirIdFile() throws IOException {
+ var ciphertextNode = tmpPath.resolve("toDecrypt.c9r");
+ when(dirIdBackup.read(ciphertextNode)) //
+ .thenThrow(TestCryptoException.class) //
+ .thenThrow(IllegalStateException.class);
+ Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode));
+ Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode));
+ }
+
+ @Test
+ @DisplayName("If the ciphertextName cannot be decrypted, throw FileSystemException")
+ public void notDecryptableCiphertext() throws IOException {
+ var name = "toDecrypt";
+ var ciphertextNode = tmpPath.resolve(name + ".c9s");
+ var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'};
+ var expectedException = new IOException("Inflation failed");
+ when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId);
+ when(longFileNameProvider.inflate(ciphertextNode)).thenThrow(expectedException);
+
+ var actual = Assertions.assertThrows(IOException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode));
+ Assertions.assertEquals(expectedException, actual);
+ }
+
+ @Test
+ @DisplayName("If inflating the shortened Name throws exception, it is rethrown")
+ public void inflateThrows() throws IOException {
+ var name = "toDecrypt";
+ var ciphertextNode = tmpPath.resolve(name + ".c9r");
+ var dirId = new byte[]{'f', 'o', 'o', 'b', 'a', 'r'};
+ when(dirIdBackup.read(ciphertextNode)).thenReturn(dirId);
+ when(fileNameCryptor.decryptFilename(any(), eq(name), eq(dirId))).thenThrow(TestCryptoException.class);
+
+ Assertions.assertThrows(FileSystemException.class, () -> testObjSpy.decryptFilenameInternal(ciphertextNode));
+ verify(fileNameCryptor).decryptFilename(any(), eq(name), eq(dirId));
+ }
+
+ @Nested
+ public class TestValidation {
+
+ Path p = mock(Path.class, "/absolute/path/to/ciphertext.c9r");
+
+ @BeforeEach
+ public void beforeEach() {
+ doReturn(true).when(testObjSpy).belongsToVault(p);
+ doReturn(true).when(testObjSpy).isAtCipherNodeLevel(p);
+ doReturn(true).when(testObjSpy).hasCipherNodeExtension(p);
+ doReturn(true).when(testObjSpy).hasMinimumFileNameLength(p);
+ }
+
+ @Test
+ @DisplayName("If node is not part of the vault, validation fails")
+ public void validateNotVaultFile() {
+ doReturn(false).when(testObjSpy).belongsToVault(p);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p));
+ verify(testObjSpy).belongsToVault(any());
+ }
+
+ @Test
+ @DisplayName("If node is on the wrong level, validation fails")
+ public void validateWrongLevel() {
+ doReturn(false).when(testObjSpy).isAtCipherNodeLevel(p);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p));
+ verify(testObjSpy).isAtCipherNodeLevel(any());
+ }
+
+
+ @Test
+ @DisplayName("If node has wrong file extension, validation fails")
+ public void validateWrongExtension() {
+ doReturn(false).when(testObjSpy).hasCipherNodeExtension(p);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p));
+ verify(testObjSpy).hasCipherNodeExtension(any());
+ }
+
+ @Test
+ @DisplayName("If filename is too short, validation fails")
+ public void validateTooShort() {
+ doReturn(false).when(testObjSpy).hasMinimumFileNameLength(p);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> testObjSpy.validatePath(p));
+ verify(testObjSpy).hasMinimumFileNameLength(any());
+ }
+ }
+
+ @Nested
+ public class IsAtCipherNodeLevel {
+
+ @TempDir
+ Path tmpDir;
+
+ @Test
+ @DisplayName("cipherNodeLevel test requires an absolute path")
+ public void requiresAbsolutePath() {
+ var relativePath = Path.of("relative/path");
+ Assertions.assertThrows(IllegalArgumentException.class, () -> testObj.isAtCipherNodeLevel(relativePath));
+ }
+
+ @Test
+ public void success() {
+ when(vaultPath.getNameCount()).thenReturn(tmpDir.getNameCount());
+ var p = tmpDir.resolve("d/AA/BBBBBBBBBBBBBBB/encrypted.file");
+ Assertions.assertTrue(testObj.isAtCipherNodeLevel(p));
+ }
+
+ @Test
+ public void failure() {
+ when(vaultPath.getNameCount()).thenReturn(tmpDir.getNameCount());
+ var p = tmpDir.resolve("d/AA/other.file");
+ Assertions.assertFalse(testObj.isAtCipherNodeLevel(p));
+ }
+ }
+
+ @ParameterizedTest
+ @DisplayName("Only c9r and c9s are accepted file extensions")
+ @CsvSource(value = {"file.c9r,true", "file.c9s,true", "filec9r,false", "file.c9l,false",})
+ public void testHasCipherNodeExtension(String filename, boolean expected) {
+ var p = Path.of(filename);
+ var result = testObj.hasCipherNodeExtension(p);
+ Assertions.assertEquals(expected, result, "The filename %s is WRONGLY %s".formatted(filename, result ? "accepted" : "rejected"));
+ }
+
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java
index 48a4954b0..934e1f582 100644
--- a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDirTest.java
@@ -5,8 +5,8 @@
import org.cryptomator.cryptofs.DirectoryIdBackup;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.util.TestCryptoException;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
-import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
@@ -227,11 +227,7 @@ class RetrieveDirIdTests {
private OrphanContentDir resultSpy;
- static class MyCryptoException extends CryptoException {
-
- }
-
- static List expectedExceptions = List.of(new IOException(), new IllegalStateException(), new MyCryptoException());
+ static List expectedExceptions = List.of(new IOException(), new IllegalStateException(), new TestCryptoException());
@BeforeEach
public void init() {
diff --git a/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java b/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java
new file mode 100644
index 000000000..7197ef6e1
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/util/TestCryptoException.java
@@ -0,0 +1,11 @@
+package org.cryptomator.cryptofs.util;
+
+import org.cryptomator.cryptolib.api.CryptoException;
+
+public class TestCryptoException extends CryptoException {
+
+ public TestCryptoException() {
+ super();
+ }
+
+}