Skip to content

Commit 06cf5fb

Browse files
committed
Merge branch 'release/1.8.2'
2 parents 345d924 + 68c6297 commit 06cf5fb

File tree

10 files changed

+145
-42
lines changed

10 files changed

+145
-42
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
dist: xenial
12
language: java
23
sudo: false
34
jdk:

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>org.cryptomator</groupId>
44
<artifactId>cryptofs</artifactId>
5-
<version>1.8.1</version>
5+
<version>1.8.2</version>
66
<name>Cryptomator Crypto Filesystem</name>
77
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
88
<url>https://github.com/cryptomator/cryptofs</url>

src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,12 @@
1010
import org.cryptomator.cryptofs.fh.FileHeaderLoader;
1111
import org.cryptomator.cryptofs.fh.OpenFileModifiedDate;
1212
import org.cryptomator.cryptofs.fh.OpenFileSize;
13-
import org.cryptomator.cryptolib.Cryptors;
1413
import org.cryptomator.cryptolib.api.Cryptor;
1514
import org.slf4j.Logger;
1615
import org.slf4j.LoggerFactory;
1716

1817
import javax.inject.Inject;
1918
import java.io.IOException;
20-
import java.io.UncheckedIOException;
2119
import java.nio.ByteBuffer;
2220
import java.nio.MappedByteBuffer;
2321
import java.nio.channels.FileChannel;
@@ -68,30 +66,12 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderLoader
6866
this.exceptionsDuringWrite = exceptionsDuringWrite;
6967
this.closeListener = closeListener;
7068
this.stats = stats;
71-
updateFileSize();
7269
if (options.append()) {
7370
position = fileSize.get();
7471
}
7572
headerWritten = !options.writable();
7673
}
7774

78-
private void updateFileSize() {
79-
try {
80-
long ciphertextSize = ciphertextFileChannel.size();
81-
if (ciphertextSize == 0l) {
82-
fileSize.set(0l);
83-
} else {
84-
long cleartextSize = Cryptors.cleartextSize(ciphertextSize - cryptor.fileHeaderCryptor().headerSize(), cryptor);
85-
fileSize.set(cleartextSize);
86-
}
87-
} catch (IllegalArgumentException e) {
88-
LOG.warn("Invalid cipher text file size.", e);
89-
fileSize.set(0l);
90-
} catch (IOException e) {
91-
throw new UncheckedIOException(e);
92-
}
93-
}
94-
9575
@Override
9676
public long size() throws IOException {
9777
assertOpen();
@@ -180,7 +160,8 @@ private long writeLockedInternal(ByteSource src, long position) throws IOExcepti
180160
written += len;
181161
}
182162
long minSize = position + written;
183-
fileSize.updateAndGet(size -> max(minSize, size));
163+
long newSize = fileSize.updateAndGet(size -> max(minSize, size));
164+
assert newSize >= minSize;
184165
lastModified.set(Instant.now());
185166
stats.addBytesWritten(written);
186167
return written;
@@ -221,6 +202,7 @@ public void force(boolean metaData) throws IOException {
221202

222203
private void forceInternal(boolean metaData) throws IOException {
223204
if (isWritable()) {
205+
writeHeaderIfNeeded();
224206
chunkCache.invalidateAll(); // TODO performance: write chunks but keep them cached
225207
exceptionsDuringWrite.throwIfPresent();
226208
attrViewProvider.get().setTimes(FileTime.from(lastModified.get()), null, null);

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
*******************************************************************************/
99
package org.cryptomator.cryptofs.fh;
1010

11+
import com.google.common.base.Preconditions;
1112
import org.cryptomator.cryptofs.EffectiveOpenOptions;
1213
import org.cryptomator.cryptofs.ch.ChannelComponent;
1314
import org.cryptomator.cryptofs.ch.CleartextFileChannel;
15+
import org.cryptomator.cryptolib.Cryptors;
16+
import org.cryptomator.cryptolib.api.Cryptor;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
1419

1520
import javax.inject.Inject;
1621
import java.io.Closeable;
@@ -27,26 +32,37 @@
2732
@OpenFileScoped
2833
public class OpenCryptoFile implements Closeable {
2934

35+
private static final Logger LOG = LoggerFactory.getLogger(OpenCryptoFile.class);
36+
3037
private final FileCloseListener listener;
3138
private final AtomicReference<Instant> lastModified;
3239
private final ChunkCache chunkCache;
40+
private final Cryptor cryptor;
3341
private final ChunkIO chunkIO;
3442
private final AtomicReference<Path> currentFilePath;
3543
private final AtomicLong fileSize;
3644
private final OpenCryptoFileComponent component;
3745
private final ConcurrentMap<CleartextFileChannel, FileChannel> openChannels = new ConcurrentHashMap<>();
3846

3947
@Inject
40-
public OpenCryptoFile(FileCloseListener listener, ChunkCache chunkCache, ChunkIO chunkIO, @CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component) {
48+
public OpenCryptoFile(FileCloseListener listener, ChunkCache chunkCache, Cryptor cryptor, ChunkIO chunkIO, @CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component) {
4149
this.listener = listener;
4250
this.chunkCache = chunkCache;
51+
this.cryptor = cryptor;
4352
this.chunkIO = chunkIO;
4453
this.currentFilePath = currentFilePath;
4554
this.fileSize = fileSize;
4655
this.component = component;
4756
this.lastModified = lastModified;
4857
}
4958

59+
/**
60+
* Creates a new file channel with the given open options.
61+
*
62+
* @param options The options to use to open the file channel. For the most part these will be passed through to the ciphertext channel.
63+
* @return A new file channel. Ideally used in a try-with-resource statement. If the channel is not properly closed, this OpenCryptoFile will stay open indefinite.
64+
* @throws IOException
65+
*/
5066
public synchronized FileChannel newFileChannel(EffectiveOpenOptions options) throws IOException {
5167
Path path = currentFilePath.get();
5268

@@ -58,6 +74,7 @@ public synchronized FileChannel newFileChannel(EffectiveOpenOptions options) thr
5874
CleartextFileChannel cleartextFileChannel = null;
5975
try {
6076
ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile());
77+
initFileSize(ciphertextFileChannel);
6178
ChannelComponent channelComponent = component.newChannelComponent() //
6279
.ciphertextChannel(ciphertextFileChannel) //
6380
.openOptions(options) //
@@ -76,7 +93,36 @@ public synchronized FileChannel newFileChannel(EffectiveOpenOptions options) thr
7693
return cleartextFileChannel;
7794
}
7895

96+
/**
97+
* Called by {@link #newFileChannel(EffectiveOpenOptions)} to determine the fileSize.
98+
* <p>
99+
* Before the size is initialized (i.e. before a channel has been created), {@link #size()} must not be called.
100+
* <p>
101+
* Initialization happens at most once per open file. Subsequent invocations are no-ops.
102+
*/
103+
private void initFileSize(FileChannel ciphertextFileChannel) throws IOException {
104+
if (fileSize.get() == -1l) {
105+
LOG.trace("First channel for this openFile. Initializing file size...");
106+
long cleartextSize = 0l;
107+
try {
108+
long ciphertextSize = ciphertextFileChannel.size();
109+
if (ciphertextSize > 0l) {
110+
cleartextSize = Cryptors.cleartextSize(ciphertextSize - cryptor.fileHeaderCryptor().headerSize(), cryptor);
111+
}
112+
} catch (IllegalArgumentException e) {
113+
LOG.warn("Invalid cipher text file size. Assuming empty file.", e);
114+
assert cleartextSize == 0l;
115+
}
116+
fileSize.compareAndSet(-1l, cleartextSize);
117+
}
118+
}
119+
120+
/**
121+
* @return The size of the opened file
122+
* @throws IllegalStateException If the OpenCryptoFile {@link OpenCryptoFiles#getOrCreate(Path) has been created} without {@link #newFileChannel(EffectiveOpenOptions) creating a file channel} next.
123+
*/
79124
public long size() {
125+
Preconditions.checkState(fileSize.get() != -1l, "size must only be called after a FileChannel is created for this OpenCryptoFile");
80126
return fileSize.get();
81127
}
82128

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,9 @@ public AtomicReference<Instant> provideLastModifiedDate(@OriginalOpenFilePath Pa
5656
@Provides
5757
@OpenFileScoped
5858
@OpenFileSize
59-
public AtomicLong provideFileSize(@OriginalOpenFilePath Path originalPath, Cryptor cryptor) {
60-
long ciphertextSize = readBasicAttributes(originalPath).map(BasicFileAttributes::size).orElse(0l);
61-
if (ciphertextSize == 0) {
62-
return new AtomicLong();
63-
} else {
64-
int headerSize = cryptor.fileHeaderCryptor().headerSize();
65-
return new AtomicLong(cleartextSize(ciphertextSize - headerSize, cryptor));
66-
}
59+
public AtomicLong provideFileSize() {
60+
// will be initialized when first creating a FileChannel. See OpenCryptoFile#size()
61+
return new AtomicLong(-1l);
6762
}
6863

6964
private Optional<BasicFileAttributes> readBasicAttributes(Path path) {

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,27 @@ public class OpenCryptoFiles implements Closeable {
4040
this.component = component;
4141
}
4242

43+
/**
44+
* Gets an OpenCryptoFile (if any is opened) without creating it.
45+
* <p>
46+
* Useful if you don't want to create any FileChannel but want to check whether this file is currently opened (e.g. to get its current {@link OpenCryptoFile#size()}).
47+
*
48+
* @param ciphertextPath Path of the file which might have been opened
49+
* @return The OpenCryptoFile if opened or an empty Optional otherwise.
50+
*/
4351
public Optional<OpenCryptoFile> get(Path ciphertextPath) {
4452
Path normalizedPath = ciphertextPath.toAbsolutePath().normalize();
4553
return Optional.ofNullable(openCryptoFiles.get(normalizedPath));
4654
}
4755

56+
/**
57+
* Opens a file to {@link OpenCryptoFile#newFileChannel(EffectiveOpenOptions) retrieve a FileChannel}. If this file is already opened, a shared instance is returned.
58+
* Getting the file channel should be the next invocation, since the {@link OpenFileScoped lifecycle} of the OpenFile strictly depends on the lifecycle of the channel.
59+
*
60+
* @param ciphertextPath Path of the file to open
61+
* @return The opened file.
62+
* @see #get(Path)
63+
*/
4864
public OpenCryptoFile getOrCreate(Path ciphertextPath) {
4965
Path normalizedPath = ciphertextPath.toAbsolutePath().normalize();
5066
return openCryptoFiles.computeIfAbsent(normalizedPath, this::create); // computeIfAbsent is atomic, "create" is called at most once

src/main/java/org/cryptomator/cryptofs/fh/OpenFileScoped.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package org.cryptomator.cryptofs.fh;
22

3-
import static java.lang.annotation.RetentionPolicy.RUNTIME;
4-
3+
import javax.inject.Scope;
54
import java.lang.annotation.Documented;
65
import java.lang.annotation.Retention;
76

8-
import javax.inject.Scope;
7+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
98

9+
/**
10+
* An OpenFile is {@link OpenCryptoFiles#getOrCreate(java.nio.file.Path) created} with the sole purpose of opening a FileChannel.
11+
* <p>
12+
* When the last active file channel is closed, the OpenFile is closed. I.e. it is strictly required for anyone creating an OpenFile to get, use and close a FileChannel.
13+
*/
1014
@Scope
1115
@Documented
1216
@Retention(RUNTIME)

src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ public static void teardownClass() throws IOException {
5858
inMemoryFs.close();
5959
}
6060

61+
// tests https://github.com/cryptomator/cryptofs/issues/50
62+
@Test
63+
public void testReadWhileStillWriting() throws IOException {
64+
Path file = filePath(nextFileId());
65+
66+
try (FileChannel ch1 = FileChannel.open(file, CREATE_NEW, WRITE)) {
67+
// it actually matters that the channel writes more than one chunk size (32k)
68+
ch1.write(repeat(1).times(35000).asByteBuffer(), 0);
69+
try (FileChannel ch2 = FileChannel.open(file, READ)) {
70+
ch1.write(repeat(2).times(5000).asByteBuffer(), 35000);
71+
}
72+
}
73+
74+
try (FileChannel ch1 = FileChannel.open(file, READ)) {
75+
ByteBuffer buffer = ByteBuffer.allocate(40000);
76+
int result = ch1.read(buffer);
77+
Assertions.assertEquals(40000, result);
78+
Assertions.assertEquals(EOF, ch1.read(ByteBuffer.allocate(0)));
79+
buffer.flip();
80+
for (int i = 0; i < 40000; i++) {
81+
if (i < 35000) {
82+
Assertions.assertEquals(1, buffer.get(i), format("byte(%d) = 1", i));
83+
} else {
84+
Assertions.assertEquals(2, buffer.get(i), format("byte(%d) = 2", i));
85+
}
86+
}
87+
}
88+
}
89+
6190
// tests https://github.com/cryptomator/cryptofs/issues/48
6291
@Test
6392
public void testTruncateExistingWhileStillOpen() throws IOException {
@@ -87,6 +116,8 @@ public void testFileSizeIsZeroAfterCreatingFileChannel() throws IOException {
87116
Assertions.assertEquals(0, channel.size());
88117
Assertions.assertEquals(0, Files.size(filePath(fileId)));
89118
}
119+
120+
Assertions.assertEquals(0, Files.size(filePath(fileId)));
90121
}
91122

92123
// tests https://github.com/cryptomator/cryptofs/issues/26
@@ -99,6 +130,8 @@ public void testFileSizeIsTenAfterWritingTenBytes() throws IOException {
99130
Assertions.assertEquals(10, channel.size());
100131
Assertions.assertEquals(10, Files.size(filePath(fileId)));
101132
}
133+
134+
Assertions.assertEquals(10, Files.size(filePath(fileId)));
102135
}
103136

104137
@Test
@@ -253,13 +286,13 @@ public void testAppend(int dataSize) throws IOException {
253286
Assertions.assertEquals(dataSize, channel.size());
254287
channel.write(repeat(1).times(dataSize).asByteBuffer());
255288
channel.write(repeat(1).times(dataSize).asByteBuffer());
256-
Assertions.assertEquals(3*dataSize, channel.size());
289+
Assertions.assertEquals(3 * dataSize, channel.size());
257290
}
258291

259292
try (FileChannel channel = readableChannel(fileId)) {
260-
ByteBuffer buffer = ByteBuffer.allocate(3*dataSize);
293+
ByteBuffer buffer = ByteBuffer.allocate(3 * dataSize);
261294
int result = channel.read(buffer);
262-
Assertions.assertEquals(3*dataSize, result);
295+
Assertions.assertEquals(3 * dataSize, result);
263296
Assertions.assertEquals(EOF, channel.read(ByteBuffer.allocate(0)));
264297
}
265298
}

src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ public void setUp() throws IOException {
7575
when(fileHeaderCryptor.headerSize()).thenReturn(50);
7676
when(fileContentCryptor.cleartextChunkSize()).thenReturn(100);
7777
when(fileContentCryptor.ciphertextChunkSize()).thenReturn(110);
78-
when(ciphertextFileChannel.size()).thenReturn(160l); // initial cleartext size will be 100
7978
when(attributeViewSupplier.get()).thenReturn(attributeView);
8079
when(readWriteLock.readLock()).thenReturn(readLock);
8180
when(readWriteLock.writeLock()).thenReturn(writeLock);
@@ -291,7 +290,7 @@ public void testReadFailsIfNotReadable() throws IOException {
291290

292291
@Test
293292
public void testReadFromMultipleChunks() throws IOException {
294-
when(ciphertextFileChannel.size()).thenReturn(5_500_000_160l); // initial cleartext size will be 5_000_000_100l
293+
fileSize.set(5_000_000_100l); // initial cleartext size will be 5_000_000_100l
295294
when(options.readable()).thenReturn(true);
296295

297296
inTest = new CleartextFileChannel(ciphertextFileChannel, headerLoader, readWriteLock, cryptor, chunkCache, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);

0 commit comments

Comments
 (0)