cachedChunks;
/**
- * This lock ensures no chunks are passed between stale and active state while flushing,
- * as flushing requires iteration over both sets.
+ * We have to deal with two forms of access to the {@link #chunkCache}:
+ *
+ * - Accessing a single chunk (with a known index), e.g. during {@link #getChunk(long)}
+ * - Accessing multiple chunks, e.g. during {@link #invalidateStale()}
+ *
+ *
+ * While the former can be handled by the cache implementation (based on {@link ConcurrentMap}) just fine,
+ * we need to make sure no concurrent modification will happen accessing multiple chunks (e.g. when iterating over the entry set).
+ *
+ * This is achieved using this {@link ReadWriteLock}, where holding the {@link ReadWriteLock#readLock() shared lock} is
+ * sufficient for index-based access, while the {@link ReadWriteLock#writeLock() exclusive lock} is necessary otherwise.
*/
- private final ReadWriteLock flushLock = new ReentrantReadWriteLock();
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private final Lock sharedLock = lock.readLock(); // required when accessing a single chunk
+ private final Lock exclusiveLock = lock.writeLock(); // required when accessing multiple chunks at once
@Inject
public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSystemStats stats, BufferPool bufferPool, ExceptionsDuringWrite exceptionsDuringWrite) {
@@ -43,12 +54,21 @@ public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSyst
this.stats = stats;
this.bufferPool = bufferPool;
this.exceptionsDuringWrite = exceptionsDuringWrite;
- this.staleChunks = Caffeine.newBuilder() //
- .maximumSize(MAX_CACHED_CLEARTEXT_CHUNKS) //
+ this.chunkCache = Caffeine.newBuilder() //
+ .maximumWeight(MAX_CACHED_CLEARTEXT_CHUNKS) //
+ .weigher(this::weigh) //
+ .executor(Runnable::run) // run `evictStaleChunk` in same thread -> see https://github.com/cryptomator/cryptofs/pull/163#issuecomment-1505249736
.evictionListener(this::evictStaleChunk) //
- .build() //
- .asMap();
- this.activeChunks = new ConcurrentHashMap<>();
+ .build();
+ this.cachedChunks = chunkCache.asMap();
+ }
+
+ private int weigh(Long index, Chunk chunk) {
+ if (chunk.currentAccesses().get() > 0) {
+ return 0; // zero, if currently in use -> avoid maximum size eviction
+ } else {
+ return 1;
+ }
}
/**
@@ -60,24 +80,23 @@ public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSyst
* @throws IllegalArgumentException If {@code chunkData}'s remaining bytes is not equal to the number of bytes fitting into a chunk
*/
public Chunk putChunk(long chunkIndex, ByteBuffer chunkData) throws IllegalArgumentException {
- return activeChunks.compute(chunkIndex, (index, chunk) -> {
- // stale chunk for this index is obsolete:
- var staleChunk = staleChunks.remove(index);
- if (staleChunk != null) {
- bufferPool.recycle(staleChunk.data());
- }
- // either create completely new chunk or replace all data of existing active chunk:
- if (chunk == null) {
- chunk = new Chunk(chunkData, true, () -> releaseChunk(chunkIndex));
- } else {
- var dst = chunk.data().duplicate().clear();
- Preconditions.checkArgument(chunkData.remaining() == dst.remaining());
- dst.put(chunkData);
- chunk.dirty().set(true);
- }
- chunk.currentAccesses().incrementAndGet();
- return chunk;
- });
+ sharedLock.lock();
+ try {
+ return cachedChunks.compute(chunkIndex, (index, chunk) -> {
+ if (chunk == null) {
+ chunk = new Chunk(chunkData, true, () -> releaseChunk(chunkIndex));
+ } else {
+ var dst = chunk.data().duplicate().clear();
+ Preconditions.checkArgument(chunkData.remaining() == dst.remaining());
+ dst.put(chunkData);
+ chunk.dirty().set(true);
+ }
+ chunk.currentAccesses().incrementAndGet();
+ return chunk;
+ });
+ } finally {
+ sharedLock.unlock();
+ }
}
/**
@@ -88,33 +107,25 @@ public Chunk putChunk(long chunkIndex, ByteBuffer chunkData) throws IllegalArgum
* @throws IOException If reading or decrypting the chunk failed
*/
public Chunk getChunk(long chunkIndex) throws IOException {
- var lock = flushLock.readLock();
- lock.lock();
+ sharedLock.lock();
try {
stats.addChunkCacheAccess();
- return activeChunks.compute(chunkIndex, this::acquireInternal);
- } catch (UncheckedIOException | AuthenticationFailedException e) {
- throw new IOException(e);
+ try {
+ return cachedChunks.compute(chunkIndex, (idx, chunk) -> {
+ if (chunk == null) {
+ chunk = loadChunk(idx);
+ }
+ chunk.currentAccesses().incrementAndGet();
+ return chunk;
+ });
+ } catch (UncheckedIOException | AuthenticationFailedException e) {
+ throw new IOException(e);
+ }
} finally {
- lock.unlock();
+ sharedLock.unlock();
}
}
- private Chunk acquireInternal(Long index, Chunk activeChunk) throws AuthenticationFailedException, UncheckedIOException {
- Chunk result = activeChunk;
- if (result == null) {
- result = staleChunks.remove(index);
- assert result == null || result.currentAccesses().get() == 0;
- }
- if (result == null) {
- result = loadChunk(index);
- }
-
- assert result != null;
- result.currentAccesses().incrementAndGet();
- return result;
- }
-
private Chunk loadChunk(long chunkIndex) throws AuthenticationFailedException, UncheckedIOException {
stats.addChunkCacheMiss();
try {
@@ -126,60 +137,48 @@ private Chunk loadChunk(long chunkIndex) throws AuthenticationFailedException, U
@SuppressWarnings("resource")
private void releaseChunk(long chunkIndex) {
- var lock = flushLock.readLock();
- lock.lock();
+ sharedLock.lock();
try {
- activeChunks.compute(chunkIndex, (index, chunk) -> {
- assert chunk != null;
- var accessCnt = chunk.currentAccesses().decrementAndGet();
- if (accessCnt == 0) {
- staleChunks.put(index, chunk);
- return null; //chunk is stale, remove from active
- } else {
- assert accessCnt > 0;
- return chunk; //keep active
- }
+ cachedChunks.computeIfPresent(chunkIndex, (idx, chunk) -> {
+ chunk.currentAccesses().decrementAndGet();
+ return chunk;
});
} finally {
- lock.unlock();
+ sharedLock.unlock();
}
}
/**
* Flushes cached data (but keeps them cached).
- * @see #invalidateAll()
+ *
+ * @see #invalidateStale()
*/
public void flush() throws IOException {
- var lock = flushLock.writeLock();
- lock.lock();
- BiFunction saveUnchecked = (index, chunk) -> {
- try {
- chunkSaver.save(index, chunk);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return chunk;
- };
+ exclusiveLock.lock();
try {
- activeChunks.replaceAll(saveUnchecked);
- staleChunks.replaceAll(saveUnchecked);
+ cachedChunks.forEach((index, chunk) -> {
+ try {
+ chunkSaver.save(index, chunk);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
} catch (UncheckedIOException e) {
throw new IOException(e);
} finally {
- lock.unlock();
+ exclusiveLock.unlock();
}
}
/**
* Removes stale chunks from cache.
*/
- public void invalidateAll() {
- var lock = flushLock.writeLock();
- lock.lock();
+ public void invalidateStale() {
+ exclusiveLock.lock();
try {
- staleChunks.clear();
+ cachedChunks.entrySet().removeIf(entry -> entry.getValue().currentAccesses().get() == 0);
} finally {
- lock.unlock();
+ exclusiveLock.unlock();
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ExceptionsDuringWrite.java b/src/main/java/org/cryptomator/cryptofs/fh/ExceptionsDuringWrite.java
index b0cf0931..0f24688b 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/ExceptionsDuringWrite.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/ExceptionsDuringWrite.java
@@ -19,7 +19,7 @@ public ExceptionsDuringWrite() {
}
public synchronized void add(Exception e) {
- e.addSuppressed(e);
+ this.e.addSuppressed(e);
}
public synchronized void throwIfPresent() throws IOException {
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java b/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java
index 34abc1e1..822b8740 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/FileHeaderHolder.java
@@ -11,6 +11,7 @@
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@OpenFileScoped
@@ -21,6 +22,8 @@ public class FileHeaderHolder {
private final Cryptor cryptor;
private final AtomicReference path;
private final AtomicReference header = new AtomicReference<>();
+ private final AtomicReference encryptedHeader = new AtomicReference<>();
+ private final AtomicBoolean isPersisted = new AtomicBoolean();
@Inject
public FileHeaderHolder(Cryptor cryptor, @CurrentOpenFilePath AtomicReference path) {
@@ -36,26 +39,48 @@ public FileHeader get() {
return result;
}
- public FileHeader createNew() {
+ public ByteBuffer getEncrypted() {
+ var result = encryptedHeader.get();
+ if (result == null) {
+ throw new IllegalStateException("Header not set.");
+ }
+ return result;
+ }
+
+ FileHeader createNew() {
LOG.trace("Generating file header for {}", path.get());
FileHeader newHeader = cryptor.fileHeaderCryptor().create();
+ encryptedHeader.set(cryptor.fileHeaderCryptor().encryptHeader(newHeader).asReadOnlyBuffer()); //to prevent NONCE reuse, we already encrypt the header and cache it
header.set(newHeader);
return newHeader;
}
- public FileHeader loadExisting(FileChannel ch) throws IOException {
+
+ /**
+ * Reads, decrypts and caches the file header from the given file channel.
+ *
+ * @param ch File channel to the encrypted file
+ * @return {@link FileHeader} of the encrypted file
+ * @throws IOException if the file header cannot be read or decrypted
+ */
+ FileHeader loadExisting(FileChannel ch) throws IOException {
LOG.trace("Reading file header from {}", path.get());
ByteBuffer existingHeaderBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize());
- int read = ch.read(existingHeaderBuf, 0);
- assert read == existingHeaderBuf.capacity();
+ ch.read(existingHeaderBuf, 0);
existingHeaderBuf.flip();
try {
FileHeader existingHeader = cryptor.fileHeaderCryptor().decryptHeader(existingHeaderBuf);
+ encryptedHeader.set(existingHeaderBuf.flip().asReadOnlyBuffer()); //for consistency
header.set(existingHeader);
+ isPersisted.set(true);
return existingHeader;
} catch (IllegalArgumentException | CryptoException e) {
throw new IOException("Unable to decrypt header of file " + path.get(), e);
}
}
+ public AtomicBoolean headerIsPersisted() {
+ return isPersisted;
+ }
+
}
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
index 24d03020..a07b5db2 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
@@ -11,7 +11,6 @@
import org.cryptomator.cryptofs.EffectiveOpenOptions;
import org.cryptomator.cryptofs.ch.CleartextFileChannel;
import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.FileHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -67,27 +66,22 @@ public OpenCryptoFile(FileCloseListener listener, ChunkCache chunkCache, Cryptor
*/
public synchronized FileChannel newFileChannel(EffectiveOpenOptions options, FileAttribute>... attrs) throws IOException {
Path path = currentFilePath.get();
-
- if (options.truncateExisting()) {
- chunkCache.invalidateAll();
+ if (path == null) {
+ throw new IllegalStateException("Cannot create file channel to deleted file");
}
-
FileChannel ciphertextFileChannel = null;
CleartextFileChannel cleartextFileChannel = null;
try {
ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile(), attrs);
- final FileHeader header;
- final boolean isNewHeader;
- if (ciphertextFileChannel.size() == 0l) {
- header = headerHolder.createNew();
- isNewHeader = true;
- } else {
- header = headerHolder.loadExisting(ciphertextFileChannel);
- isNewHeader = false;
+ initFileHeader(options, ciphertextFileChannel);
+ if (options.truncateExisting()) {
+ chunkCache.invalidateStale();
+ ciphertextFileChannel.truncate(cryptor.fileHeaderCryptor().headerSize());
+ fileSize.set(0);
}
initFileSize(ciphertextFileChannel);
cleartextFileChannel = component.newChannelComponent() //
- .create(ciphertextFileChannel, header, isNewHeader, options, this::channelClosed) //
+ .create(ciphertextFileChannel, options, this::channelClosed) //
.channel();
} finally {
if (cleartextFileChannel == null) { // i.e. something didn't work
@@ -105,6 +99,23 @@ public synchronized FileChannel newFileChannel(EffectiveOpenOptions options, Fil
return cleartextFileChannel;
}
+ //visible for testing
+ void initFileHeader(EffectiveOpenOptions options, FileChannel ciphertextFileChannel) throws IOException {
+ try {
+ headerHolder.get();
+ } catch (IllegalStateException e) {
+ //first file channel to file
+ if (options.createNew() || (options.create() && ciphertextFileChannel.size() == 0)) {
+ //file did not exist, create new header
+ //file size will never be zero again, once the header is written because we retain on truncation the header
+ headerHolder.createNew();
+ } else {
+ //file must exist, load header from file
+ headerHolder.loadExisting(ciphertextFileChannel);
+ }
+ }
+ }
+
private void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
@@ -116,7 +127,7 @@ private void closeQuietly(Closeable closeable) {
}
/**
- * Called by {@link #newFileChannel(EffectiveOpenOptions)} to determine the fileSize.
+ * Called by {@link #newFileChannel(EffectiveOpenOptions, FileAttribute[])} to determine the fileSize.
*
* Before the size is initialized (i.e. before a channel has been created), {@link #size()} must not be called.
*
@@ -141,7 +152,7 @@ private void initFileSize(FileChannel ciphertextFileChannel) throws IOException
}
/**
- * @return The size of the opened file. Note that the filesize is unknown until a {@link #newFileChannel(EffectiveOpenOptions) file channel is opened}. In this case this method returns an empty optional.
+ * @return The size of the opened file. Note that the filesize is unknown until a {@link #newFileChannel(EffectiveOpenOptions, FileAttribute[])} is opened. In this case this method returns an empty optional.
*/
public Optional size() {
long val = fileSize.get();
@@ -164,8 +175,12 @@ public Path getCurrentFilePath() {
return currentFilePath.get();
}
- public void setCurrentFilePath(Path currentFilePath) {
- this.currentFilePath.set(currentFilePath);
+ /**
+ * Updates the current ciphertext file path, if it is not already set to null (i.e., the openCryptoFile is deleted)
+ * @param newFilePath new ciphertext path
+ */
+ public void updateCurrentFilePath(Path newFilePath) {
+ currentFilePath.updateAndGet(p -> p == null ? null : newFilePath);
}
private synchronized void channelClosed(CleartextFileChannel cleartextFileChannel) throws IOException {
@@ -184,7 +199,10 @@ private synchronized void channelClosed(CleartextFileChannel cleartextFileChanne
@Override
public void close() {
- listener.close(currentFilePath.get(), this);
+ var p = currentFilePath.get();
+ if(p != null) {
+ listener.close(p, this);
+ }
}
@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
index 83d587ee..aceb7484 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
@@ -28,20 +28,11 @@ public ReadWriteLock provideReadWriteLock() {
@Provides
@OpenFileScoped
- @CurrentOpenFilePath // TODO: do we still need this? only used in logging.
+ @CurrentOpenFilePath
public AtomicReference provideCurrentPath(@OriginalOpenFilePath Path originalPath) {
return new AtomicReference<>(originalPath);
}
- @Provides
- @OpenFileScoped
- public Supplier provideBasicFileAttributeViewSupplier(@CurrentOpenFilePath AtomicReference currentPath) {
- return () -> {
- Path path = currentPath.get();
- return path.getFileSystem().provider().getFileAttributeView(path, BasicFileAttributeView.class);
- };
- }
-
@Provides
@OpenFileScoped
@OpenFileModifiedDate
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java
index 4186e248..4e872250 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java
@@ -81,6 +81,20 @@ public ByteBuffer readCiphertextFile(Path ciphertextPath, EffectiveOpenOptions o
}
}
+ /**
+ * Removes a ciphertextPath to {@link OpenCryptoFile} mapping, if it exists, and sets the path of the openCryptoFile to null.
+ *
+ * @param ciphertextPath The ciphertext file path to invalidate
+ */
+ public void delete(Path ciphertextPath) {
+ openCryptoFiles.compute(ciphertextPath, (p, openFile) -> {
+ if (openFile != null) {
+ openFile.updateCurrentFilePath(null);
+ }
+ return null;
+ });
+ }
+
/**
* Prepares to update any open file references during a move operation.
* MUST be invoked using a try-with-resource statement and committed after the physical file move succeeded.
@@ -137,7 +151,7 @@ public void commit() {
throw new IllegalStateException();
}
if (openCryptoFile != null) {
- openCryptoFile.setCurrentFilePath(dst);
+ openCryptoFile.updateCurrentFilePath(dst);
}
openCryptoFiles.remove(src, openCryptoFile);
committed = true;
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 93216d8b..02bda852 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -8,7 +8,6 @@
*******************************************************************************/
package org.cryptomator.cryptofs;
-import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import org.cryptomator.cryptofs.util.ByteBuffers;
import org.cryptomator.cryptolib.api.Masterkey;
@@ -33,6 +32,7 @@
import org.mockito.Mockito;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.nio.ByteBuffer;
@@ -49,8 +49,11 @@
import java.util.Arrays;
import java.util.List;
import java.util.Random;
+import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -251,17 +254,17 @@ public void testWriteFromSecondChannelWhileStillOpen() throws IOException {
}
}
- // tests https://github.com/cryptomator/cryptofs/issues/160
+ //tests changes made in https://github.com/cryptomator/cryptofs/pull/166
@Test
- @DisplayName("TRUNCATE_EXISTING leads to new file header")
+ @DisplayName("TRUNCATE_EXISTING does not produce invalid ciphertext")
public void testNewFileHeaderWhenTruncateExisting() throws IOException {
try (var ch1 = FileChannel.open(file, CREATE_NEW, WRITE)) {
ch1.write(StandardCharsets.UTF_8.encode("this content will be truncated soon"), 0);
ch1.force(true);
- try (var ch2 = FileChannel.open(file, CREATE, WRITE, TRUNCATE_EXISTING)) { // re-roll file header
+ try (var ch2 = FileChannel.open(file, CREATE, WRITE, TRUNCATE_EXISTING)) {
ch2.write(StandardCharsets.UTF_8.encode("hello"), 0);
}
- ch1.write(StandardCharsets.UTF_8.encode(" world"), 5); // should use new file key
+ ch1.write(StandardCharsets.UTF_8.encode(" world"), 5);
}
try (var ch3 = FileChannel.open(file, READ)) {
@@ -547,6 +550,83 @@ public void testConcurrentRead() throws IOException, InterruptedException {
}));
}
- }
+ //https://github.com/cryptomator/cryptofs/issues/168
+ @Test
+ @DisplayName("Opening two file channels simultaneously and close afterwards retains ciphertext readability")
+ public void testOpeningTwoChannelsRetainsCiphertextReadability() throws IOException {
+ var content = StandardCharsets.UTF_8.encode("two channels sitting on the wall").asReadOnlyBuffer();
+ ByteBuffer bytesRead = ByteBuffer.allocate(content.limit());
+
+ try (var ch = FileChannel.open(file, READ, WRITE, CREATE_NEW)) {
+ System.out.println("Openend channel " + ch);
+ try (var ch2 = FileChannel.open(file, WRITE)) {
+ }
+ ch.write(content, 0);
+ }
+ Assertions.assertDoesNotThrow(() -> {
+ try (var ch = FileChannel.open(file, READ)) {
+ ch.read(bytesRead, 0);
+ }
+ });
+ }
+
+ //https://github.com/cryptomator/cryptofs/issues/169
+ @Test
+ public void testClosingChannelOfDeletedFileDoesNotThrow() {
+ Assertions.assertDoesNotThrow(() -> {
+ try (var ch = FileChannel.open(file, CREATE_NEW, WRITE)) {
+ ch.write(ByteBuffer.wrap("delete me".getBytes(StandardCharsets.UTF_8)));
+ Files.delete(file);
+ }
+ });
+ Assertions.assertTrue(Files.notExists(file));
+ }
+
+ //https://github.com/cryptomator/cryptofs/issues/170
+ @Test
+ public void testWriteThenDeleteThenRead() throws IOException {
+ var bufToWrite = StandardCharsets.UTF_8.encode("delete me");
+ final int bytesRead;
+ try (var ch = FileChannel.open(file, CREATE_NEW, WRITE)) {
+ ch.write(bufToWrite);
+ Files.delete(file);
+ try (var ch2 = fileSystem.provider().newFileChannel(file, Set.of(CREATE, READ, WRITE))) {
+ bytesRead = ch2.read(ByteBuffer.allocate(bufToWrite.capacity()));
+ }
+ }
+ Assertions.assertEquals(-1, bytesRead);
+ }
+
+ @RepeatedTest(50)
+ public void testConcurrentWriteAndTruncate() throws IOException {
+ AtomicBoolean keepWriting = new AtomicBoolean(true);
+ ByteBuffer buf = ByteBuffer.wrap("the quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8));
+ var executor = Executors.newCachedThreadPool();
+ try (FileChannel writingChannel = FileChannel.open(file, WRITE, CREATE)) {
+ executor.submit(() -> {
+ while (keepWriting.get()) {
+ try {
+ writingChannel.write(buf);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ buf.flip();
+ }
+ });
+ try (FileChannel truncatingChannel = FileChannel.open(file, WRITE, TRUNCATE_EXISTING)) {
+ keepWriting.set(false);
+ }
+ executor.shutdown();
+ }
+
+ Assertions.assertDoesNotThrow(() -> {
+ try (FileChannel readingChannel = FileChannel.open(file, READ)) {
+ var dst = ByteBuffer.allocate(buf.capacity());
+ readingChannel.read(dst);
+ }
+ });
+ }
+
+ }
}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index a133999f..12cc3ef2 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -72,6 +72,7 @@
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -205,7 +206,7 @@ public void testCleartextDirectory() throws IOException {
Path result = inTest.getCiphertextPath(cleartext);
Assertions.assertEquals(ciphertext, result);
- Mockito.verify(cryptoPathMapper,never()).getCiphertextFilePath(any());
+ Mockito.verify(cryptoPathMapper, never()).getCiphertextFilePath(any());
}
}
@@ -558,6 +559,7 @@ public class Delete {
private final CryptoPath cleartextPath = mock(CryptoPath.class, "cleartext");
private final Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
private final Path ciphertextDirFilePath = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
+ private final Path ciphertextFilePath = mock(Path.class, "d/00/00/path.c9r");
private final Path ciphertextDirPath = mock(Path.class, "d/FF/FF/");
private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext");
private final FileSystem physicalFs = mock(FileSystem.class);
@@ -574,6 +576,7 @@ public void setup() throws IOException {
when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFilePath);
when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath);
when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath);
+ when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath);
when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFilePath);
when(cryptoPathMapper.getCiphertextDir(cleartextPath)).thenReturn(new CiphertextDirectory("foo", ciphertextDirPath));
when(physicalFsProv.readAttributes(ciphertextRawPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr);
@@ -590,10 +593,12 @@ public void testDeleteRootFails() {
public void testDeleteExistingFile() throws IOException {
when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE);
when(physicalFsProv.deleteIfExists(ciphertextRawPath)).thenReturn(true);
+ doNothing().when(openCryptoFiles).delete(Mockito.any());
inTest.delete(cleartextPath);
verify(readonlyFlag).assertWritable();
+ verify(openCryptoFiles).delete(ciphertextFilePath);
verify(physicalFsProv).deleteIfExists(ciphertextRawPath);
}
diff --git a/src/test/java/org/cryptomator/cryptofs/EffectiveOpenOptionsTest.java b/src/test/java/org/cryptomator/cryptofs/EffectiveOpenOptionsTest.java
index f8de714a..08af690f 100644
--- a/src/test/java/org/cryptomator/cryptofs/EffectiveOpenOptionsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/EffectiveOpenOptionsTest.java
@@ -294,6 +294,24 @@ public void testTruncateExisting() throws IOException {
MatcherAssert.assertThat(inTest.createOpenOptionsForEncryptedFile(), containsInAnyOrder(READ));
}
+ @Test
+ public void testTruncateExistingAndWrite() throws IOException {
+ EffectiveOpenOptions inTest = EffectiveOpenOptions.from(Set.of(TRUNCATE_EXISTING, WRITE), falseReadonlyFlag);
+
+ Assertions.assertFalse(inTest.append());
+ Assertions.assertFalse(inTest.create());
+ Assertions.assertFalse(inTest.createNew());
+ Assertions.assertFalse(inTest.deleteOnClose());
+ Assertions.assertFalse(inTest.noFollowLinks());
+ Assertions.assertFalse(inTest.readable());
+ Assertions.assertFalse(inTest.syncData());
+ Assertions.assertFalse(inTest.syncDataAndMetadata());
+ Assertions.assertTrue(inTest.truncateExisting());
+ Assertions.assertTrue(inTest.writable());
+
+ MatcherAssert.assertThat(inTest.createOpenOptionsForEncryptedFile(), containsInAnyOrder(READ, WRITE));
+ }
+
@Test
public void testWrite() throws IOException {
EffectiveOpenOptions inTest = EffectiveOpenOptions.from(Set.of(WRITE), falseReadonlyFlag);
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
index fe9ac96f..8b07565a 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
@@ -6,9 +6,9 @@
import org.cryptomator.cryptofs.fh.Chunk;
import org.cryptomator.cryptofs.fh.ChunkCache;
import org.cryptomator.cryptofs.fh.ExceptionsDuringWrite;
+import org.cryptomator.cryptofs.fh.FileHeaderHolder;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileContentCryptor;
-import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
@@ -32,9 +32,13 @@
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
+import java.nio.file.spi.FileSystemProvider;
import java.time.Instant;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
@@ -43,6 +47,9 @@
import static org.hamcrest.CoreMatchers.is;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -59,12 +66,13 @@ public class CleartextFileChannelTest {
private FileHeaderCryptor fileHeaderCryptor = mock(FileHeaderCryptor.class);
private FileContentCryptor fileContentCryptor = mock(FileContentCryptor.class);
private FileChannel ciphertextFileChannel = mock(FileChannel.class);
- private FileHeader header = mock(FileHeader.class);
- private boolean mustWriteHeader = true;
+ 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 AtomicReference currentFilePath = new AtomicReference<>(filePath);
private AtomicLong fileSize = new AtomicLong(100);
private AtomicReference lastModified = new AtomicReference<>(Instant.ofEpochMilli(0));
- private Supplier attributeViewSupplier = mock(Supplier.class);
private BasicFileAttributeView attributeView = mock(BasicFileAttributeView.class);
private ExceptionsDuringWrite exceptionsDuringWrite = mock(ExceptionsDuringWrite.class);
private ChannelCloseListener closeListener = mock(ChannelCloseListener.class);
@@ -80,13 +88,19 @@ public void setUp() throws IOException {
when(chunkCache.putChunk(Mockito.anyLong(), Mockito.any())).thenAnswer(invocation -> new Chunk(invocation.getArgument(1), true, () -> {}));
when(bufferPool.getCleartextBuffer()).thenAnswer(invocation -> ByteBuffer.allocate(100));
when(fileHeaderCryptor.headerSize()).thenReturn(50);
+ when(headerHolder.headerIsPersisted()).thenReturn(headerIsPersisted);
+ when(headerIsPersisted.getAndSet(anyBoolean())).thenReturn(true);
when(fileContentCryptor.cleartextChunkSize()).thenReturn(100);
when(fileContentCryptor.ciphertextChunkSize()).thenReturn(110);
- when(attributeViewSupplier.get()).thenReturn(attributeView);
+ var fs = Mockito.mock(FileSystem.class);
+ 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(readWriteLock.readLock()).thenReturn(readLock);
when(readWriteLock.writeLock()).thenReturn(writeLock);
- inTest = new CleartextFileChannel(ciphertextFileChannel, header, mustWriteHeader, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);
+ inTest = new CleartextFileChannel(ciphertextFileChannel, headerHolder, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, currentFilePath, exceptionsDuringWrite, closeListener, stats);
}
@Test
@@ -339,7 +353,7 @@ public void testReadFromMultipleChunks() throws IOException {
fileSize.set(5_000_000_100l); // initial cleartext size will be 5_000_000_100l
when(options.readable()).thenReturn(true);
- inTest = new CleartextFileChannel(ciphertextFileChannel, header, mustWriteHeader, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);
+ inTest = new CleartextFileChannel(ciphertextFileChannel, headerHolder, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, currentFilePath, exceptionsDuringWrite, closeListener, stats);
ByteBuffer buf = ByteBuffer.allocate(10);
// A read from frist chunk:
@@ -483,6 +497,8 @@ public void writeAfterEof() throws IOException {
public void testWriteHeaderIfNeeded() throws IOException {
when(options.writable()).thenReturn(true);
+ when(headerIsPersisted.get()).thenReturn(false).thenReturn(true).thenReturn(true);
+
inTest.force(true);
inTest.force(true);
inTest.force(true);
@@ -490,11 +506,26 @@ public void testWriteHeaderIfNeeded() throws IOException {
Mockito.verify(ciphertextFileChannel, Mockito.times(1)).write(Mockito.any(), Mockito.eq(0l));
}
+ @Test
+ @DisplayName("If writing header fails, it is indicated as not persistent")
+ public void testWriteHeaderFailsResetsPersistenceState() throws IOException {
+ when(options.writable()).thenReturn(true);
+ when(headerIsPersisted.get()).thenReturn(false);
+ doNothing().when(headerIsPersisted).set(anyBoolean());
+ when(ciphertextFileChannel.write(any(), anyLong())).thenThrow(new IOException("writing failed"));
+
+ Assertions.assertThrows(IOException.class, () -> inTest.force(true));
+
+ Mockito.verify(ciphertextFileChannel, Mockito.times(1)).write(Mockito.any(), Mockito.eq(0l));
+ Mockito.verify(headerIsPersisted, Mockito.never()).set(anyBoolean());
+ }
+
@Test
@DisplayName("don't write header if it is already written")
public void testDontRewriteHeader() throws IOException {
when(options.writable()).thenReturn(true);
- inTest = new CleartextFileChannel(ciphertextFileChannel, header, false, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);
+ when(headerIsPersisted.get()).thenReturn(true);
+ inTest = new CleartextFileChannel(ciphertextFileChannel, headerHolder, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, currentFilePath, exceptionsDuringWrite, closeListener, stats);
inTest.force(true);
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
index d4cf0615..6f39f9f4 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
@@ -8,15 +8,13 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.AccessDeniedException;
-import java.time.Duration;
-import java.util.concurrent.CountDownLatch;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -156,22 +154,16 @@ public void testClosingActiveChunkThatIsReferencedTwice() throws IOException, Au
verifyNoMoreInteractions(bufferPool);
}
- @RepeatedTest(30)
- @DisplayName("chunk.close() triggers eviction of LRU stale chunk")
+ @Test
+ @DisplayName("chunk.close() triggers eviction of some stale chunk")
public void testClosingActiveChunkTriggersEvictionOfStaleChunk() throws IOException, AuthenticationFailedException {
- var cdl = new CountDownLatch(1);
- Mockito.doAnswer(invocation -> {
- cdl.countDown();
- return null;
- }).when(chunkSaver).save(Mockito.anyLong(), Mockito.any());
-
activeChunk1.close();
- Assertions.assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
- cdl.await();
- });
- verify(chunkSaver).save(42L, staleChunk42);
- verify(bufferPool).recycle(staleChunk42.data());
+ // we can't know _which_ stale chunk gets evicted. see https://github.com/ben-manes/caffeine/issues/583
+ ArgumentCaptor chunkCaptor = ArgumentCaptor.forClass(Chunk.class);
+ ArgumentCaptor indexCaptor = ArgumentCaptor.forClass(Long.class);
+ verify(chunkSaver).save(indexCaptor.capture(), chunkCaptor.capture());
+ verify(bufferPool).recycle(chunkCaptor.getValue().data());
verifyNoMoreInteractions(chunkSaver);
}
@@ -221,11 +213,11 @@ public void testFlushKeepsItemInCacheDespiteIOException() throws IOException, Au
}
@Test
- @DisplayName("invalidateAll() flushes stale chunks but keeps active chunks")
- public void testInvalidateAll() throws IOException, AuthenticationFailedException {
+ @DisplayName("invalidateStale() flushes stale chunks but keeps active chunks")
+ public void testInvalidateStale() throws IOException, AuthenticationFailedException {
when(chunkLoader.load(Mockito.anyLong())).thenReturn(ByteBuffer.allocate(0));
- inTest.invalidateAll();
+ inTest.invalidateStale();
Assertions.assertSame(activeChunk1, inTest.getChunk(1L));
Assertions.assertNotSame(staleChunk42, inTest.getChunk(42L));
@@ -253,17 +245,6 @@ public void testPutChunkReturnsActiveChunk() {
Assertions.assertTrue(chunk.isDirty());
}
- @Test
- @DisplayName("putChunk() recycles stale chunk if present")
- public void testPutChunkRecyclesStaleChunk() {
- var chunk = inTest.putChunk(42L, ByteBuffer.allocate(0));
-
- Assertions.assertNotSame(staleChunk42, chunk);
- Assertions.assertEquals(1, chunk.currentAccesses().get());
- Assertions.assertTrue(chunk.isDirty());
- verify(bufferPool).recycle(staleChunk42.data());
- }
-
@Test
@DisplayName("putChunk() returns new chunk if neither stale nor active")
public void testPutChunkReturnsNewChunk() {
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
index bad4f2cd..ed93fac4 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
@@ -42,6 +42,7 @@ public class FileHeaderHolderTest {
@BeforeEach
public void setup() throws IOException {
when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor);
+ when(fileHeaderCryptor.encryptHeader(Mockito.any())).thenReturn(ByteBuffer.wrap(new byte[0]));
}
@Nested
@@ -75,6 +76,9 @@ public void testLoadExisting() throws IOException, AuthenticationFailedException
Assertions.assertSame(headerToLoad, loadedHeader3);
verify(fileHeaderCryptor, times(1)).decryptHeader(Mockito.any());
+ Assertions.assertNotNull(inTest.get());
+ Assertions.assertNotNull(inTest.getEncrypted());
+ Assertions.assertTrue(inTest.headerIsPersisted().get());
}
}
@@ -106,6 +110,9 @@ public void testCreateNew() {
Assertions.assertSame(headerToCreate, createdHeader3);
verify(fileHeaderCryptor, times(1)).create();
+ Assertions.assertNotNull(inTest.get());
+ Assertions.assertNotNull(inTest.getEncrypted());
+ Assertions.assertFalse(inTest.headerIsPersisted().get());
}
}
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
index afd5616d..3de6a2f9 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
@@ -9,6 +9,7 @@
import org.cryptomator.cryptofs.ch.CleartextFileChannel;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
@@ -35,7 +36,10 @@
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class OpenCryptoFileTest {
@@ -46,10 +50,10 @@ public class OpenCryptoFileTest {
private FileCloseListener closeListener = mock(FileCloseListener.class);
private ChunkCache chunkCache = mock(ChunkCache.class);
private Cryptor cryptor = mock(Cryptor.class);
+ private FileHeaderCryptor fileHeaderCryptor = mock(FileHeaderCryptor.class);
private FileHeaderHolder headerHolder = mock(FileHeaderHolder.class);
- private FileHeader header = mock(FileHeader.class);
private ChunkIO chunkIO = mock(ChunkIO.class);
- private AtomicLong fileSize = new AtomicLong(-1l);
+ private AtomicLong fileSize = Mockito.mock(AtomicLong.class);
private AtomicReference lastModified = new AtomicReference(Instant.ofEpochMilli(0));
private OpenCryptoFileComponent openCryptoFileComponent = mock(OpenCryptoFileComponent.class);
private ChannelComponent.Factory channelComponentFactory = mock(ChannelComponent.Factory.class);
@@ -88,12 +92,103 @@ public void testCloseImmediatelyIfOpeningFirstChannelFails() {
verify(closeListener).close(CURRENT_FILE_PATH.get(), openCryptoFile);
}
+ @Test
+ @DisplayName("Opening a file channel with TRUNCATE_EXISTING sets the file size to 0")
+ public void testFileSizeZerodOnTruncateExisting() throws IOException {
+ EffectiveOpenOptions options = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING), readonlyFlag);
+ Mockito.when(headerHolder.get()).thenReturn(Mockito.mock(FileHeader.class));
+ Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor);
+ Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(42);
+ Mockito.when(openCryptoFileComponent.newChannelComponent()).thenReturn(channelComponentFactory);
+ Mockito.when(channelComponentFactory.create(any(), any(), any())).thenReturn(channelComponent);
+ Mockito.when(channelComponent.channel()).thenReturn(mock(CleartextFileChannel.class));
+ OpenCryptoFile openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent);
+
+ openCryptoFile.newFileChannel(options);
+ verify(fileSize).set(0L);
+ }
+
+ @Nested
+ @DisplayName("Testing ::initFileHeader")
+ public class InitFilHeaderTests {
+
+ EffectiveOpenOptions options = Mockito.mock(EffectiveOpenOptions.class);
+ FileChannel cipherFileChannel = Mockito.mock(FileChannel.class, "cipherFilechannel");
+ OpenCryptoFile inTest = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent);
+
+ @Test
+ @DisplayName("Skip file header init, if the file header already exists in memory")
+ public void testInitFileHeaderExisting() throws IOException {
+ var header = Mockito.mock(FileHeader.class);
+ Mockito.when(headerHolder.get()).thenReturn(header);
+
+ inTest.initFileHeader(options, cipherFileChannel);
+
+ Mockito.verify(headerHolder, never()).loadExisting(any());
+ Mockito.verify(headerHolder, never()).createNew();
+ }
+
+ @Test
+ @DisplayName("Load file header from file, if not present and neither create nor create_new set")
+ public void testInitFileHeaderLoad() throws IOException {
+ Mockito.when(headerHolder.get()).thenThrow(new IllegalStateException("no Header set"));
+ Mockito.when(options.createNew()).thenReturn(false);
+ Mockito.when(options.create()).thenReturn(false);
+
+ inTest.initFileHeader(options, cipherFileChannel);
+
+ Mockito.verify(headerHolder, times(1)).loadExisting(cipherFileChannel);
+ Mockito.verify(headerHolder, never()).createNew();
+ }
+
+ @Test
+ @DisplayName("Create new file header, if not present and create_new set")
+ public void testInitFileHeaderCreateNew() throws IOException {
+ Mockito.when(headerHolder.get()).thenThrow(new IllegalStateException("no Header set"));
+ Mockito.when(options.createNew()).thenReturn(true);
+
+ inTest.initFileHeader(options, cipherFileChannel);
+
+ Mockito.verify(headerHolder, times(1)).createNew();
+ Mockito.verify(headerHolder, never()).loadExisting(any());
+ }
+
+ @Test
+ @DisplayName("Create new file header, if not present, create set and channel.size() == 0")
+ public void testInitFileHeaderCreateAndSize0() throws IOException {
+ Mockito.when(headerHolder.get()).thenThrow(new IllegalStateException("no Header set"));
+ Mockito.when(options.createNew()).thenReturn(false);
+ Mockito.when(options.create()).thenReturn(true);
+ Mockito.when(cipherFileChannel.size()).thenReturn(0L);
+
+ inTest.initFileHeader(options, cipherFileChannel);
+
+ Mockito.verify(headerHolder, times(1)).createNew();
+ Mockito.verify(headerHolder, never()).loadExisting(any());
+ }
+
+ @Test
+ @DisplayName("Load file header, if create is set but channel has size > 0")
+ public void testInitFileHeaderCreateAndSizeGreater0() throws IOException {
+ Mockito.when(headerHolder.get()).thenThrow(new IllegalStateException("no Header set"));
+ Mockito.when(options.createNew()).thenReturn(false);
+ Mockito.when(options.create()).thenReturn(true);
+ Mockito.when(cipherFileChannel.size()).thenReturn(42L);
+
+ inTest.initFileHeader(options, cipherFileChannel);
+
+ Mockito.verify(headerHolder, times(1)).loadExisting(cipherFileChannel);
+ Mockito.verify(headerHolder, never()).createNew();
+ }
+ }
+
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("FileChannels")
public class FileChannelFactoryTest {
+ private final AtomicLong realFileSize = new AtomicLong(-1L);
private OpenCryptoFile openCryptoFile;
private CleartextFileChannel cleartextFileChannel;
private AtomicReference listener;
@@ -101,15 +196,17 @@ public class FileChannelFactoryTest {
@BeforeAll
public void setup() throws IOException {
- openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, fileSize, lastModified, openCryptoFileComponent);
+ FS = Jimfs.newFileSystem("OpenCryptoFileTest.FileChannelFactoryTest", Configuration.unix().toBuilder().setAttributeViews("basic", "posix").build());
+ CURRENT_FILE_PATH = new AtomicReference<>(FS.getPath("currentFile"));
+ openCryptoFile = new OpenCryptoFile(closeListener, chunkCache, cryptor, headerHolder, chunkIO, CURRENT_FILE_PATH, realFileSize, lastModified, openCryptoFileComponent);
cleartextFileChannel = mock(CleartextFileChannel.class);
listener = new AtomicReference<>();
ciphertextChannel = new AtomicReference<>();
Mockito.when(openCryptoFileComponent.newChannelComponent()).thenReturn(channelComponentFactory);
- Mockito.when(channelComponentFactory.create(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> {
+ Mockito.when(channelComponentFactory.create(Mockito.any(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> {
ciphertextChannel.set(invocation.getArgument(0));
- listener.set(invocation.getArgument(4));
+ listener.set(invocation.getArgument(2));
return channelComponent;
});
Mockito.when(channelComponent.channel()).thenReturn(cleartextFileChannel);
@@ -168,10 +265,12 @@ public void testGetSizeAfterCreatingSecondFileChannel() {
@Order(20)
@DisplayName("TRUNCATE_EXISTING leads to chunk cache invalidation")
public void testTruncateExistingInvalidatesChunkCache() throws IOException {
+ Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor);
+ Mockito.when(fileHeaderCryptor.headerSize()).thenReturn(43);
Files.write(CURRENT_FILE_PATH.get(), new byte[0]);
EffectiveOpenOptions options = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE), readonlyFlag);
openCryptoFile.newFileChannel(options);
- verify(chunkCache).invalidateAll();
+ verify(chunkCache).invalidateStale();
}
@Test