diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9f706d7d..8396a94f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -19,6 +19,7 @@ updates:
- "org.apache.maven.plugins:*"
- "org.jacoco:jacoco-maven-plugin"
- "org.owasp:dependency-check-maven"
+ - "me.fabriciorby:maven-surefire-junit5-tree-reporter"
java-production-dependencies:
patterns:
- "*"
@@ -30,6 +31,7 @@ updates:
- "org.mockito:*"
- "org.hamcrest:*"
- "com.google.jimfs:jimfs"
+ - "me.fabriciorby:maven-surefire-junit5-tree-reporter"
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 88f8da8a..da527ddf 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,6 +1,8 @@
name: Build
on:
- [push]
+ push:
+ pull_request_target:
+ types: [labeled]
jobs:
build:
name: Build and Test
@@ -12,7 +14,7 @@ jobs:
show-progress: false
- uses: actions/setup-java@v4
with:
- java-version: 17
+ java-version: 21
distribution: 'temurin'
cache: 'maven'
- name: Cache SonarCloud packages
@@ -42,8 +44,8 @@ jobs:
path: target/*.jar
- name: Create release
if: startsWith(github.ref, 'refs/tags/')
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }}
generate_release_notes: true
- prerelease: true
\ No newline at end of file
+ prerelease: true
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 237bcac3..9bfa8088 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -22,7 +22,7 @@ jobs:
show-progress: false
- uses: actions/setup-java@v4
with:
- java-version: 17
+ java-version: 21
distribution: 'temurin'
cache: 'maven'
- name: Initialize CodeQL
diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml
index 4e7e7d0f..601975f4 100644
--- a/.github/workflows/dependency-check.yml
+++ b/.github/workflows/dependency-check.yml
@@ -14,7 +14,7 @@ jobs:
with:
runner-os: 'ubuntu-latest'
java-distribution: 'temurin'
- java-version: 17
+ java-version: 21
secrets:
nvd-api-key: ${{ secrets.NVD_API_KEY }}
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml
index 9eb1b363..07727e4c 100644
--- a/.github/workflows/publish-central.yml
+++ b/.github/workflows/publish-central.yml
@@ -16,14 +16,12 @@ jobs:
show-progress: false
- uses: actions/setup-java@v4
with:
- java-version: 17
+ java-version: 21
distribution: 'temurin'
cache: 'maven'
server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml
server-username: MAVEN_USERNAME # env variable for username in deploy
server-password: MAVEN_PASSWORD # env variable for token in deploy
- gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
- gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Enforce project version ${{ github.event.inputs.tag }}
run: mvn versions:set -B -DnewVersion=${{ github.event.inputs.tag }}
- name: Deploy
@@ -36,4 +34,5 @@ jobs:
--add-opens=java.desktop/java.awt.font=ALL-UNNAMED
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
- MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
\ No newline at end of file
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
+ MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml
index e0f8b793..59b312e4 100644
--- a/.github/workflows/publish-github.yml
+++ b/.github/workflows/publish-github.yml
@@ -12,11 +12,9 @@ jobs:
show-progress: false
- uses: actions/setup-java@v4
with:
- java-version: 17
+ java-version: 21
distribution: 'temurin'
cache: 'maven'
- gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
- gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Enforce project version ${{ github.event.release.tag_name }}
run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }}
- name: Deploy
@@ -24,6 +22,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
+ MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 67e1e611..9dc782bb 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,5 +8,5 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 6ed04e88..3050a0db 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptofs/badge.svg)](https://snyk.io/test/github/cryptomator/cryptofs)
**CryptoFS:** Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme.
+For more info about the encryption scheme, read the [docs](https://docs.cryptomator.org/en/latest/security/vault/).
## Features
@@ -98,7 +99,7 @@ For more details on how to use the constructed `FileSystem`, you may consult the
### Dependencies
-* Java 17
+* Java 21
* Maven 3
### Run Maven
diff --git a/pom.xml b/pom.xml
index 95107f48..a0fc375d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 2.6.9
+ 2.7.0
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
@@ -15,27 +15,27 @@
UTF-8
- 17
+ 21
- 2.1.2
+ 2.2.0
4.4.0
- 2.49
- 32.1.3-jre
+ 2.51.1
+ 33.2.1-jre
3.1.8
- 2.0.12
+ 2.0.13
- 5.10.2
- 5.2.0
- 2.2
+ 5.10.3
+ 5.12.0
+ 3.0
1.3.0
- 9.0.9
- 1.2.1
- 0.8.11
- 1.6.13
+ 10.0.3
+ 1.3.0
+ 0.8.12
+ 1.7.0
@@ -114,7 +114,7 @@
org.mockito
- mockito-inline
+ mockito-core
${mockito.version}
test
@@ -143,7 +143,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.12.1
+ 3.13.0
true
@@ -158,7 +158,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.2.5
+ 3.3.1
me.fabriciorby
@@ -181,11 +181,11 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.3.0
+ 3.4.2
maven-source-plugin
- 3.3.0
+ 3.3.1
attach-sources
@@ -197,7 +197,7 @@
maven-javadoc-plugin
- 3.6.3
+ 3.8.0
attach-javadocs
@@ -252,7 +252,7 @@
true
true
suppression.xml
- ${env.NVD_API_KEY}
+ NVD_API_KEY
@@ -300,7 +300,7 @@
maven-gpg-plugin
- 3.1.0
+ 3.2.4
sign-artifacts
@@ -309,10 +309,7 @@
sign
-
- --pinentry-mode
- loopback
-
+ bc
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 5cbfb094..abe11b30 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -19,6 +19,7 @@
// https://github.com/javax-inject/javax-inject/issues/33
// May be provided by another lib during runtime
requires static javax.inject;
+ requires java.compiler;
exports org.cryptomator.cryptofs;
exports org.cryptomator.cryptofs.common;
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java
index 1e44bfa1..f621b770 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java
@@ -48,10 +48,10 @@ public BasicFileAttributes readAttributes() throws IOException {
@Override
public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException {
readonlyFlag.assertWritable();
- getCiphertextAttributeView(BasicFileAttributeView.class).setTimes(lastModifiedTime, lastAccessTime, createTime);
if (lastModifiedTime != null) {
getOpenCryptoFile().ifPresent(file -> file.setLastModifiedTime(lastModifiedTime));
}
+ getCiphertextAttributeView(BasicFileAttributeView.class).setTimes(lastModifiedTime, lastAccessTime, createTime);
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java b/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java
index 4933e1bc..3ee1c008 100644
--- a/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java
+++ b/src/main/java/org/cryptomator/cryptofs/ch/ChannelCloseListener.java
@@ -1,11 +1,9 @@
package org.cryptomator.cryptofs.ch;
-import java.io.IOException;
-
@FunctionalInterface
public interface ChannelCloseListener {
- void closed(CleartextFileChannel channel) throws IOException;
+ void closed(CleartextFileChannel channel);
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
index 4723e80b..44d165a8 100644
--- a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
+++ b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
@@ -29,6 +29,8 @@
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
+import java.util.Iterator;
+import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
@@ -233,7 +235,8 @@ private void forceInternal(boolean metaData) throws IOException {
*
* @throws IOException
*/
- private void flush() throws IOException {
+ @VisibleForTesting
+ void flush() throws IOException {
if (isWritable()) {
writeHeaderIfNeeded();
chunkCache.flush();
@@ -322,20 +325,38 @@ long beginOfChunk(long cleartextPos) {
@Override
protected void implCloseChannel() throws IOException {
+ var closeActions = List.of(this::flush, //
+ super::implCloseChannel, //
+ () -> closeListener.closed(this), //
+ ciphertextFileChannel::close, //
+ this::tryPersistLastModified);
+ tryAll(closeActions.iterator());
+ }
+
+ private void tryPersistLastModified() {
try {
- flush();
- ciphertextFileChannel.force(true);
+ persistLastModified();
+ } catch (NoSuchFileException nsfe) {
+ //no-op, see https://github.com/cryptomator/cryptofs/issues/169
+ } catch (IOException e) {
+ LOG.warn("Failed to persist last modified timestamp for encrypted file: {}", e.getMessage());
+ }
+ }
+
+ private void tryAll(Iterator actions) throws IOException {
+ if (actions.hasNext()) {
try {
- persistLastModified();
- } catch (NoSuchFileException nsfe) {
- //no-op, see https://github.com/cryptomator/cryptofs/issues/169
- } catch (IOException e) {
- //only best effort attempt
- LOG.warn("Failed to persist last modified timestamp for encrypted file: {}", e.getMessage());
+ actions.next().run();
+ } finally {
+ tryAll(actions);
}
- } finally {
- super.implCloseChannel();
- closeListener.closed(this);
}
}
+
+ @FunctionalInterface
+ private interface CloseAction {
+
+ void run() throws IOException;
+ }
+
}
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ChunkIO.java b/src/main/java/org/cryptomator/cryptofs/fh/ChunkIO.java
index 8ec707f8..e1e73c63 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/ChunkIO.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/ChunkIO.java
@@ -52,21 +52,11 @@ int write(ByteBuffer src, long position) throws IOException {
}
private FileChannel getReadableChannel() {
- Iterator iter = readableChannels.iterator();
- if (iter.hasNext()) {
- return iter.next();
- } else {
- throw new NonReadableChannelException();
- }
+ return readableChannels.stream().findFirst().orElseThrow(NonReadableChannelException::new);
}
private FileChannel getWritableChannel() {
- Iterator iter = writableChannels.iterator();
- if (iter.hasNext()) {
- return iter.next();
- } else {
- throw new NonWritableChannelException();
- }
+ return writableChannels.stream().findFirst().orElseThrow(NonWritableChannelException::new);
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
index a07b5db2..14e5d072 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
@@ -183,17 +183,13 @@ public void updateCurrentFilePath(Path newFilePath) {
currentFilePath.updateAndGet(p -> p == null ? null : newFilePath);
}
- private synchronized void channelClosed(CleartextFileChannel cleartextFileChannel) throws IOException {
- try {
- FileChannel ciphertextFileChannel = openChannels.remove(cleartextFileChannel);
- if (ciphertextFileChannel != null) {
- chunkIO.unregisterChannel(ciphertextFileChannel);
- ciphertextFileChannel.close();
- }
- } finally {
- if (openChannels.isEmpty()) {
- close();
- }
+ private synchronized void channelClosed(CleartextFileChannel cleartextFileChannel) {
+ FileChannel ciphertextFileChannel = openChannels.remove(cleartextFileChannel);
+ if (ciphertextFileChannel != null) {
+ chunkIO.unregisterChannel(ciphertextFileChannel);
+ }
+ if (openChannels.isEmpty()) {
+ close();
}
}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index e694130f..5095b5cd 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -72,7 +72,6 @@
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -959,6 +958,7 @@ public void copyDirectory() throws IOException {
when(physicalFsProv.newFileChannel(Mockito.eq(ciphertextDestinationDirFile), Mockito.any(), any(FileAttribute[].class))).thenReturn(ciphertextTargetDirDirFileFileChannel);
when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY);
when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class);
+ when(physicalFsProv.exists(ciphertextTargetParent)).thenReturn(true);
Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile);
inTest.copy(cleartextSource, cleartextDestination);
@@ -1007,6 +1007,7 @@ public void moveDirectoryCopyBasicAttributes() throws IOException {
when(physicalFsProv.readAttributes(Mockito.same(ciphertextSourceDir), Mockito.same(BasicFileAttributes.class), any(LinkOption[].class))).thenReturn(srcAttrs);
when(physicalFsProv.getFileAttributeView(Mockito.same(ciphertextDestinationDir), Mockito.same(BasicFileAttributeView.class), any(LinkOption[].class))).thenReturn(dstAttrView);
when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), any(FileAttribute[].class))).thenReturn(ciphertextTargetDirDirFileFileChannel);
+ when(physicalFsProv.exists(ciphertextTargetParent)).thenReturn(true);
inTest.copy(cleartextSource, cleartextDestination, StandardCopyOption.COPY_ATTRIBUTES);
@@ -1027,6 +1028,7 @@ public void moveDirectoryCopyFileOwnerAttributes() throws IOException {
when(physicalFsProv.getFileAttributeView(Mockito.same(ciphertextSourceDir), Mockito.same(FileOwnerAttributeView.class), any(LinkOption[].class))).thenReturn(srcAttrsView);
when(physicalFsProv.getFileAttributeView(Mockito.same(ciphertextDestinationDir), Mockito.same(FileOwnerAttributeView.class), any(LinkOption[].class))).thenReturn(dstAttrView);
when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), any(FileAttribute[].class))).thenReturn(ciphertextTargetDirDirFileFileChannel);
+ when(physicalFsProv.exists(ciphertextTargetParent)).thenReturn(true);
inTest.copy(cleartextSource, cleartextDestination, StandardCopyOption.COPY_ATTRIBUTES);
@@ -1050,6 +1052,7 @@ public void moveDirectoryCopyPosixAttributes() throws IOException {
when(physicalFsProv.readAttributes(Mockito.same(ciphertextSourceDir), Mockito.same(PosixFileAttributes.class), any(LinkOption[].class))).thenReturn(srcAttrs);
when(physicalFsProv.getFileAttributeView(Mockito.same(ciphertextDestinationDir), Mockito.same(PosixFileAttributeView.class), any(LinkOption[].class))).thenReturn(dstAttrView);
when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), any(FileAttribute[].class))).thenReturn(ciphertextTargetDirDirFileFileChannel);
+ when(physicalFsProv.exists(ciphertextTargetParent)).thenReturn(true);
inTest.copy(cleartextSource, cleartextDestination, StandardCopyOption.COPY_ATTRIBUTES);
@@ -1073,6 +1076,7 @@ public void moveDirectoryCopyDosAttributes() throws IOException {
when(physicalFsProv.readAttributes(Mockito.same(ciphertextSourceDir), Mockito.same(DosFileAttributes.class), any(LinkOption[].class))).thenReturn(srcAttrs);
when(physicalFsProv.getFileAttributeView(Mockito.same(ciphertextDestinationDir), Mockito.same(DosFileAttributeView.class), any(LinkOption[].class))).thenReturn(dstAttrView);
when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), any(FileAttribute[].class))).thenReturn(ciphertextTargetDirDirFileFileChannel);
+ when(physicalFsProv.exists(ciphertextTargetParent)).thenReturn(true);
inTest.copy(cleartextSource, cleartextDestination, StandardCopyOption.COPY_ATTRIBUTES);
@@ -1168,6 +1172,7 @@ public void createDirectoryIfPathCiphertextFileDoesExistThrowsFileAlreadyExcepti
when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("foo", ciphertextParent));
when(ciphertextParent.getFileSystem()).thenReturn(fileSystem);
doThrow(new FileAlreadyExistsException(path.toString())).when(cryptoPathMapper).assertNonExisting(path);
+ when(provider.exists(ciphertextParent)).thenReturn(true);
FileAlreadyExistsException e = Assertions.assertThrows(FileAlreadyExistsException.class, () -> {
inTest.createDirectory(path);
@@ -1177,7 +1182,7 @@ public void createDirectoryIfPathCiphertextFileDoesExistThrowsFileAlreadyExcepti
@Test
public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOException {
- Path ciphertextParent = mock(Path.class, "ciphertextParent");
+ Path ciphertextParent = mock(Path.class, "d/00/00");
Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
@@ -1187,7 +1192,7 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath));
- when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath));
+ when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextParent));
when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class);
when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile);
@@ -1197,6 +1202,7 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);
+ when(provider.exists(ciphertextParent)).thenReturn(true);
inTest.createDirectory(path);
@@ -1206,7 +1212,7 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
@Test
public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() throws IOException {
- Path ciphertextParent = mock(Path.class, "ciphertextParent");
+ Path ciphertextParent = mock(Path.class, "d/00/00");
Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
@@ -1216,7 +1222,7 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath));
- when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath));
+ when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextParent));
when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class);
when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile);
@@ -1226,15 +1232,18 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);
+ when(provider.exists(ciphertextParent)).thenReturn(true);
- // make createDirectory with an FileSystemException during Files.createDirectories(ciphertextDirPath)
- doThrow(new IOException()).when(provider).createDirectory(ciphertextDirPath);
+ // make createDirectory with an FileSystemException during Files.createDirectories(ciphertextContentDir)
+ doThrow(new IOException()).when(provider).readAttributesIfExists(ciphertextDirPath, BasicFileAttributes.class);
+ doThrow(new FileAlreadyExistsException("very specific")).when(provider).createDirectory(ciphertextDirPath);
when(ciphertextDirPath.toAbsolutePath()).thenReturn(ciphertextDirPath);
when(ciphertextDirPath.getParent()).thenReturn(null);
- Assertions.assertThrows(IOException.class, () -> {
+ var exception = Assertions.assertThrows(FileAlreadyExistsException.class, () -> {
inTest.createDirectory(path);
});
+ Assertions.assertEquals("very specific", exception.getMessage());
verify(readonlyFlag).assertWritable();
verify(provider).delete(ciphertextDirFile);
verify(dirIdProvider).delete(ciphertextDirFile);
@@ -1243,7 +1252,7 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
@Test
public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException {
- Path ciphertextParent = mock(Path.class, "ciphertextParent");
+ Path ciphertextParent = mock(Path.class, "d/00/00");
Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
@@ -1254,7 +1263,7 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException
when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(cipherDirObject);
- when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath));
+ when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextParent));
when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class);
when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile);
@@ -1264,6 +1273,7 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);
+ when(provider.exists(ciphertextParent)).thenReturn(true);
inTest.createDirectory(path);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
index e2da36a8..32241fb8 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
@@ -290,6 +290,7 @@ public void testGetCiphertextFileTypeForDirectory() throws IOException {
Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.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(true);
CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
@@ -305,6 +306,7 @@ public void testGetCiphertextFileTypeForSymlink() throws IOException {
Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class);
Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class));
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);
@@ -322,6 +324,7 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException {
Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class));
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("LONGCLEAR"), Mockito.any())).thenReturn(Strings.repeat("A", 1000));
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);
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
index 89213ddf..a19e508d 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
@@ -43,7 +43,6 @@
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
-import java.util.function.Supplier;
import static org.hamcrest.CoreMatchers.is;
import static org.mockito.ArgumentMatchers.any;
@@ -71,7 +70,7 @@ public class CleartextFileChannelTest {
private FileHeaderHolder headerHolder = mock(FileHeaderHolder.class);
private AtomicBoolean headerIsPersisted = mock(AtomicBoolean.class);
private EffectiveOpenOptions options = mock(EffectiveOpenOptions.class);
- private Path filePath = Mockito.mock(Path.class,"/foo/bar");
+ private Path filePath = Mockito.mock(Path.class, "/foo/bar");
private AtomicReference currentFilePath = new AtomicReference<>(filePath);
private AtomicLong fileSize = new AtomicLong(100);
private AtomicReference lastModified = new AtomicReference<>(Instant.ofEpochMilli(0));
@@ -98,7 +97,7 @@ public void setUp() throws IOException {
var fsProvider = Mockito.mock(FileSystemProvider.class);
when(filePath.getFileSystem()).thenReturn(fs);
when(fs.provider()).thenReturn(fsProvider);
- when(fsProvider.getFileAttributeView(filePath,BasicFileAttributeView.class)).thenReturn(attributeView);
+ when(fsProvider.getFileAttributeView(filePath, BasicFileAttributeView.class)).thenReturn(attributeView);
when(readWriteLock.readLock()).thenReturn(readLock);
when(readWriteLock.writeLock()).thenReturn(writeLock);
@@ -236,20 +235,26 @@ public void testForceWithoutMetadataDoesntUpdatesLastModifiedTime() throws IOExc
public class Close {
@Test
- public void testCloseTriggersCloseListener() throws IOException {
- inTest.implCloseChannel();
+ @DisplayName("IOException during flush cleans up, persists lastModified and rethrows")
+ public void testCloseIoExceptionFlush() throws IOException {
+ var inSpy = Mockito.spy(inTest);
+ Mockito.doThrow(IOException.class).when(inSpy).flush();
+
+ Assertions.assertThrows(IOException.class, () -> inSpy.implCloseChannel());
- verify(closeListener).closed(inTest);
+ verify(closeListener).closed(inSpy);
+ verify(ciphertextFileChannel).close();
+ verify(inSpy).persistLastModified();
}
@Test
- @DisplayName("On close, first flush channel, then persist lastModified")
- public void testCloseFlushBeforePersist() throws IOException {
+ @DisplayName("On close, first close channel, then persist lastModified")
+ public void testCloseCipherChannelCloseBeforePersist() throws IOException {
var inSpy = spy(inTest);
inSpy.implCloseChannel();
var ordering = inOrder(inSpy, ciphertextFileChannel);
- ordering.verify(ciphertextFileChannel).force(true);
+ ordering.verify(ciphertextFileChannel).close();
ordering.verify(inSpy).persistLastModified();
}
@@ -264,6 +269,20 @@ public void testCloseUpdatesLastModifiedTimeIfWriteable() throws IOException {
verify(attributeView).setTimes(Mockito.eq(fileTime), Mockito.any(), Mockito.isNull());
}
+ @Test
+ @DisplayName("IOException on persisting lastModified during close is ignored")
+ public void testCloseExceptionOnLastModifiedPersistenceIgnored() throws IOException {
+ when(options.writable()).thenReturn(true);
+ lastModified.set(Instant.ofEpochMilli(123456789000l));
+
+ var inSpy = Mockito.spy(inTest);
+ Mockito.doThrow(IOException.class).when(inSpy).persistLastModified();
+
+ Assertions.assertDoesNotThrow(() -> inSpy.implCloseChannel());
+ verify(closeListener).closed(inSpy);
+ verify(ciphertextFileChannel).close();
+ }
+
@Test
public void testCloseDoesNotUpdateLastModifiedTimeIfReadOnly() throws IOException {
when(options.writable()).thenReturn(false);