diff --git a/pom.xml b/pom.xml
index 1f92d11b..a5d72544 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 1.9.2
+ 1.9.3
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
index e1c0caa1..28ab66f4 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
@@ -3,10 +3,12 @@
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.cryptofs.attr.AttributeViewComponent;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
import java.nio.file.Path;
+import java.util.Set;
@CryptoFileSystemScoped
@Subcomponent(modules = {CryptoFileSystemModule.class})
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 9cc9de44..ef323f29 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -164,7 +164,7 @@ public static void initialize(Path pathToVault, String masterkeyFilename, byte[]
if (!Files.isDirectory(pathToVault)) {
throw new NotDirectoryException(pathToVault.toString());
}
- new FileSystemCapabilityChecker().checkCapabilities(pathToVault);
+ new FileSystemCapabilityChecker().assertAllCapabilities(pathToVault);
try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) {
// save masterkey file:
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
@@ -293,8 +293,6 @@ public String getScheme() {
public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) throws IOException {
CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
-
- new FileSystemCapabilityChecker().checkCapabilities(parsedUri.pathToVault());
// TODO remove implicit initialization in 2.0.0
initializeFileSystemIfRequired(parsedUri, properties);
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
index dad25a59..ce52f6e6 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
@@ -7,7 +7,7 @@
import javax.inject.Singleton;
@Singleton
-@Component
+@Component(modules = {CryptoFileSystemProviderModule.class})
interface CryptoFileSystemProviderComponent {
CryptoFileSystems fileSystems();
@@ -16,8 +16,6 @@ interface CryptoFileSystemProviderComponent {
CopyOperation copyOperation();
- CryptoFileSystemComponent.Builder newCryptoFileSystemComponent();
-
@Component.Builder
interface Builder {
@BindsInstance
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
new file mode 100644
index 00000000..9bd7b33e
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
@@ -0,0 +1,18 @@
+package org.cryptomator.cryptofs;
+
+import dagger.Module;
+import dagger.Provides;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+
+import javax.inject.Singleton;
+
+@Module(subcomponents = {CryptoFileSystemComponent.class})
+public class CryptoFileSystemProviderModule {
+
+ @Provides
+ @Singleton
+ public FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
+ return new FileSystemCapabilityChecker();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 61e55295..867afd1c 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -1,5 +1,9 @@
package org.cryptomator.cryptofs;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
@@ -7,6 +11,8 @@
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Path;
+import java.util.EnumSet;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -14,25 +20,28 @@
@Singleton
class CryptoFileSystems {
-
- private final CryptoFileSystemProviderComponent cryptoFileSystemProviderComponent;
+
+ private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystems.class);
private final ConcurrentMap fileSystems = new ConcurrentHashMap<>();
+ private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder;
+ private final FileSystemCapabilityChecker capabilityChecker;
@Inject
- public CryptoFileSystems(CryptoFileSystemProviderComponent cryptoFileSystemProviderComponent) {
- this.cryptoFileSystemProviderComponent = cryptoFileSystemProviderComponent;
+ public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder, FileSystemCapabilityChecker capabilityChecker) {
+ this.cryptoFileSystemComponentBuilder = cryptoFileSystemComponentBuilder;
+ this.capabilityChecker = capabilityChecker;
}
public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException {
try {
Path normalizedPathToVault = pathToVault.normalize();
+ CryptoFileSystemProperties adjustedProperites = adjustForCapabilities(normalizedPathToVault, properties);
return fileSystems.compute(normalizedPathToVault, (key, value) -> {
if (value == null) {
- return cryptoFileSystemProviderComponent //
- .newCryptoFileSystemComponent() //
+ return cryptoFileSystemComponentBuilder //
.pathToVault(key) //
- .properties(properties) //
+ .properties(adjustedProperites) //
.provider(provider) //
.build() //
.cryptoFileSystem();
@@ -44,6 +53,23 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
throw new IOException("Error during file system creation.", e);
}
}
+
+ private CryptoFileSystemProperties adjustForCapabilities(Path pathToVault, CryptoFileSystemProperties originalProperties) throws FileSystemCapabilityChecker.MissingCapabilityException {
+ if (!originalProperties.readonly()) {
+ try {
+ capabilityChecker.assertWriteAccess(pathToVault);
+ return originalProperties;
+ } catch (FileSystemCapabilityChecker.MissingCapabilityException e) {
+ capabilityChecker.assertReadAccess(pathToVault);
+ LOG.warn("No write access to vault. Fallback to read-only access.");
+ Set flags = EnumSet.copyOf(originalProperties.flags());
+ flags.add(CryptoFileSystemProperties.FileSystemFlags.READONLY);
+ return CryptoFileSystemProperties.cryptoFileSystemPropertiesFrom(originalProperties).withFlags(flags).build();
+ }
+ } else {
+ return originalProperties;
+ }
+ }
public void remove(CryptoFileSystemImpl cryptoFileSystem) {
fileSystems.values().remove(cryptoFileSystem);
diff --git a/src/main/java/org/cryptomator/cryptofs/ReadonlyFlag.java b/src/main/java/org/cryptomator/cryptofs/ReadonlyFlag.java
index bd1de9fa..913ee717 100644
--- a/src/main/java/org/cryptomator/cryptofs/ReadonlyFlag.java
+++ b/src/main/java/org/cryptomator/cryptofs/ReadonlyFlag.java
@@ -18,13 +18,10 @@ public class ReadonlyFlag {
private final boolean readonly;
@Inject
- public ReadonlyFlag(CryptoFileSystemProperties properties, @PathToVault Path pathToVault) {
+ public ReadonlyFlag(CryptoFileSystemProperties properties) {
if (properties.readonly()) {
LOG.info("Vault opened readonly.");
readonly = true;
- } else if (!Files.isWritable(pathToVault)) {
- LOG.warn("Vault directory is write-protected.");
- readonly = true;
} else {
LOG.debug("Vault opened for read and write.");
readonly = false;
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java
index 28f07f72..c5df2ac6 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java
@@ -8,9 +8,8 @@
*******************************************************************************/
package org.cryptomator.cryptofs.attr;
-import org.cryptomator.cryptofs.CryptoFileSystemComponent;
-import org.cryptomator.cryptofs.CryptoPath;
import org.cryptomator.cryptofs.CryptoFileSystemScoped;
+import org.cryptomator.cryptofs.CryptoPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
index d9531a6d..443bcbcb 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -8,6 +8,7 @@
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -17,13 +18,27 @@ public class FileSystemCapabilityChecker {
private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
public enum Capability {
+ /**
+ * File system allows read access
+ * @since 1.9.3
+ */
+ READ_ACCESS,
+
+ /**
+ * File system allows write access
+ * @since 1.9.3
+ */
+ WRITE_ACCESS,
+
/**
* File system supports filenames with ≥ 230 chars.
+ * @since @since 1.9.2
*/
LONG_FILENAMES,
/**
* File system supports paths with ≥ 400 chars.
+ * @since @since 1.9.2
*/
LONG_PATHS,
}
@@ -36,38 +51,78 @@ public enum Capability {
* @implNote Only short-running tests with constant time are performed
* @since 1.9.2
*/
- public void checkCapabilities(Path pathToVault) throws MissingCapabilityException {
+ public void assertAllCapabilities(Path pathToVault) throws MissingCapabilityException {
+ assertReadAccess(pathToVault);
+ assertWriteAccess(pathToVault);
+ assertLongFilenameSupport(pathToVault);
+ assertLongFilePathSupport(pathToVault);
+ }
+
+ /**
+ * Checks whether the underlying filesystem allows reading the given dir.
+ * @param pathToVault Path to a vault's storage location
+ * @throws MissingCapabilityException if the check fails
+ * @since 1.9.3
+ */
+ public void assertReadAccess(Path pathToVault) throws MissingCapabilityException {
+ try (DirectoryStream ds = Files.newDirectoryStream(pathToVault)) {
+ assert ds != null;
+ } catch (IOException e) {
+ throw new MissingCapabilityException(pathToVault, Capability.READ_ACCESS);
+ }
+ }
+
+ /**
+ * Checks whether the underlying filesystem allows writing to the given dir.
+ * @param pathToVault Path to a vault's storage location
+ * @throws MissingCapabilityException if the check fails
+ * @since 1.9.3
+ */
+ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityException {
Path checkDir = pathToVault.resolve("c");
try {
- checkLongFilenames(checkDir);
- checkLongFilePaths(checkDir);
+ Files.createDirectory(checkDir);
+ } catch (IOException e) {
+ throw new MissingCapabilityException(checkDir, Capability.WRITE_ACCESS);
} finally {
- try {
- MoreFiles.deleteRecursively(checkDir, RecursiveDeleteOption.ALLOW_INSECURE);
- } catch (IOException e) {
- LOG.warn("Failed to clean up " + checkDir, e);
- }
+ deleteSilently(checkDir);
}
}
- private void checkLongFilenames(Path checkDir) throws MissingCapabilityException {
+ public void assertLongFilenameSupport(Path pathToVault) throws MissingCapabilityException {
String longFileName = Strings.repeat("a", 226) + ".c9r";
+ Path checkDir = pathToVault.resolve("c");
Path p = checkDir.resolve(longFileName);
try {
Files.createDirectories(p);
} catch (IOException e) {
throw new MissingCapabilityException(p, Capability.LONG_FILENAMES);
+ } finally {
+ deleteSilently(checkDir);
}
}
- private void checkLongFilePaths(Path checkDir) throws MissingCapabilityException {
+ public void assertLongFilePathSupport(Path pathToVault) throws MissingCapabilityException {
String longFileName = Strings.repeat("a", 96) + ".c9r";
String longPath = Joiner.on('/').join(longFileName, longFileName, longFileName, longFileName);
+ Path checkDir = pathToVault.resolve("c");
Path p = checkDir.resolve(longPath);
try {
Files.createDirectories(p);
} catch (IOException e) {
throw new MissingCapabilityException(p, Capability.LONG_PATHS);
+ } finally {
+ deleteSilently(checkDir);
+ }
+ }
+
+ private void deleteSilently(Path dir) {
+ try {
+ if (Files.exists(dir)) {
+ MoreFiles.deleteRecursively(dir, RecursiveDeleteOption.ALLOW_INSECURE);
+ }
+ } catch (IOException e) {
+ LOG.warn("Failed to clean up " + dir, e);
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
index 8937eb60..0cd5475c 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -94,7 +94,7 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws
* @throws IOException if an I/O error occurs migrating the vault
*/
public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
- fsCapabilityChecker.checkCapabilities(pathToVault);
+ fsCapabilityChecker.assertAllCapabilities(pathToVault);
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 9c1c909b..56687d7b 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -1,5 +1,6 @@
package org.cryptomator.cryptofs;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -25,14 +26,13 @@ public class CryptoFileSystemsTest {
private final CryptoFileSystemComponent cryptoFileSystemComponent = mock(CryptoFileSystemComponent.class);
private final CryptoFileSystemImpl cryptoFileSystem = mock(CryptoFileSystemImpl.class);
- private final CryptoFileSystemProviderComponent cryptoFileSystemProviderComponent = mock(CryptoFileSystemProviderComponent.class);
private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder = mock(CryptoFileSystemComponent.Builder.class);
+ private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class);
- private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemProviderComponent);
+ private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker);
@BeforeEach
public void setup() {
- when(cryptoFileSystemProviderComponent.newCryptoFileSystemComponent()).thenReturn(cryptoFileSystemComponentBuilder);
when(cryptoFileSystemComponentBuilder.provider(any())).thenReturn(cryptoFileSystemComponentBuilder);
when(cryptoFileSystemComponentBuilder.pathToVault(any())).thenReturn(cryptoFileSystemComponentBuilder);
when(cryptoFileSystemComponentBuilder.properties(any())).thenReturn(cryptoFileSystemComponentBuilder);
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadonlyFlagTest.java b/src/test/java/org/cryptomator/cryptofs/ReadonlyFlagTest.java
index 27b132de..db400eb8 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadonlyFlagTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadonlyFlagTest.java
@@ -1,80 +1,40 @@
package org.cryptomator.cryptofs;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
-import org.mockito.Mockito;
+import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.AccessMode;
-import java.nio.file.FileStore;
-import java.nio.file.FileSystem;
-import java.nio.file.Path;
import java.nio.file.ReadOnlyFileSystemException;
-import java.nio.file.spi.FileSystemProvider;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ReadonlyFlagTest {
- private FileStore fileStore = mock(FileStore.class);
- private FileSystemProvider provider = mock(FileSystemProvider.class);
- private FileSystem fileSystem = mock(FileSystem.class);
- private Path path = mock(Path.class, "test-path");
-
private CryptoFileSystemProperties properties = mock(CryptoFileSystemProperties.class);
- @BeforeEach
- public void setup() throws IOException {
- when(path.getFileSystem()).thenReturn(fileSystem);
- when(fileSystem.provider()).thenReturn(provider);
- when(provider.getFileStore(path)).thenReturn(fileStore);
- }
-
@DisplayName("isSet()")
- @ParameterizedTest(name = "readonlyFlag: {0}, writeProtected: {1} -> mounted readonly {2}")
- @CsvSource({
- "false, false, false",
- "true, false, true",
- "false, true, true",
- "true, true, true",
- })
- public void testIsSet(boolean readonlyFlag, boolean writeProtected, boolean expectedResult) throws IOException {
- when(properties.readonly()).thenReturn(readonlyFlag);
- if (writeProtected) {
- Mockito.doThrow(new AccessDeniedException(path.toString())).when(provider).checkAccess(path, AccessMode.WRITE);
- }
- ReadonlyFlag inTest = new ReadonlyFlag(properties, path);
+ @ParameterizedTest(name = "readonlyFlag: {0} -> mounted readonly {0}")
+ @ValueSource(booleans = {true, false})
+ public void testIsSet(boolean readonly) {
+ when(properties.readonly()).thenReturn(readonly);
+ ReadonlyFlag inTest = new ReadonlyFlag(properties);
boolean result = inTest.isSet();
- MatcherAssert.assertThat(result, CoreMatchers.is(expectedResult));
- Assertions.assertEquals(expectedResult, result);
+ Assertions.assertEquals(readonly, result);
}
@DisplayName("assertWritable()")
- @ParameterizedTest(name = "readonlyFlag: {0}, writeProtected: {1} -> mounted readonly {2}")
- @CsvSource({
- "false, false, false",
- "true, false, true",
- "false, true, true",
- "true, true, true",
- })
- public void testAssertWritable(boolean readonlyFlag, boolean writeProtected, boolean expectedResult) throws IOException {
- when(properties.readonly()).thenReturn(readonlyFlag);
- if (writeProtected) {
- Mockito.doThrow(new AccessDeniedException(path.toString())).when(provider).checkAccess(path, AccessMode.WRITE);
- }
- ReadonlyFlag inTest = new ReadonlyFlag(properties, path);
+ @ParameterizedTest(name = "readonlyFlag: {0} -> mounted readonly {0}")
+ @ValueSource(booleans = {true, false})
+ public void testAssertWritable(boolean readonly) {
+ when(properties.readonly()).thenReturn(readonly);
+ ReadonlyFlag inTest = new ReadonlyFlag(properties);
- if (expectedResult) {
+ if (readonly) {
Assertions.assertThrows(ReadOnlyFileSystemException.class, () -> {
inTest.assertWritable();
});
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index f15ae16f..6a90919d 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -89,7 +89,7 @@ public void testMigrateWithFailingCapabilitiesCheck() throws IOException {
Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
Exception expected = new FileSystemCapabilityChecker.MissingCapabilityException(pathToVault, FileSystemCapabilityChecker.Capability.LONG_FILENAMES);
- Mockito.doThrow(expected).when(fsCapabilityChecker).checkCapabilities(pathToVault);
+ Mockito.doThrow(expected).when(fsCapabilityChecker).assertAllCapabilities(pathToVault);
Exception thrown = Assertions.assertThrows(FileSystemCapabilityChecker.MissingCapabilityException.class, () -> {
migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {});