Skip to content

Commit fa335fa

Browse files
Merge branch 'release/1.8.6'
2 parents 8b9d605 + c493ebb commit fa335fa

12 files changed

+146
-66
lines changed

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.5</version>
5+
<version>1.8.6</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/ConflictResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
122122
}
123123
LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
124124
Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
125-
longFileNameProvider.persistCachedIfDeflated(resolved);
125+
longFileNameProvider.getCached(resolved).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
126126
return resolved;
127127
} catch (AuthenticationFailedException e) {
128128
// not decryptable, no need to resolve any kind of conflict

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import java.util.Collections;
5757
import java.util.EnumSet;
5858
import java.util.Map;
59+
import java.util.Optional;
5960
import java.util.Set;
6061
import java.util.stream.Collectors;
6162

@@ -304,7 +305,7 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
304305
// create dir if and only if the dirFile has been created right now (not if it has been created before):
305306
try {
306307
Files.createDirectories(ciphertextDir.path);
307-
longFileNameProvider.persistCachedIfDeflated(ciphertextDirFile);
308+
longFileNameProvider.getCached(ciphertextDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
308309
} catch (IOException e) {
309310
// make sure there is no orphan dir file:
310311
Files.delete(ciphertextDirFile);
@@ -355,7 +356,7 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp
355356
} else {
356357
// might also throw FileAlreadyExists:
357358
FileChannel ch = openCryptoFiles.getOrCreate(ciphertextPath).newFileChannel(options);
358-
longFileNameProvider.persistCachedIfDeflated(ciphertextPath);
359+
longFileNameProvider.getCached(ciphertextPath).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
359360
return ch;
360361
}
361362
}
@@ -423,8 +424,10 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
423424
Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.SYMLINK);
424425
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
425426
CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new);
427+
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile);
426428
Files.copy(ciphertextSourceFile, ciphertextTargetFile, resolvedOptions);
427-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
429+
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);
430+
428431
} else {
429432
CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource);
430433
CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget);
@@ -436,17 +439,19 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
436439
private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
437440
Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE);
438441
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
442+
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile);
439443
Files.copy(ciphertextSourceFile, ciphertextTargetFile, options);
440-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
444+
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);
441445
}
442446

443447
private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
444448
// DIRECTORY (non-recursive as per contract):
445449
Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.DIRECTORY);
446450
if (Files.notExists(ciphertextTargetDirFile)) {
447451
// create new:
452+
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetDirFile);
448453
createDirectory(cleartextTarget);
449-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
454+
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);
450455
} else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
451456
// keep existing (if empty):
452457
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
@@ -525,7 +530,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
525530
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
526531
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
527532
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
528-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
533+
longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
529534
twoPhaseMove.commit();
530535
}
531536
}
@@ -537,7 +542,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
537542
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
538543
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
539544
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
540-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
545+
longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
541546
twoPhaseMove.commit();
542547
}
543548
}
@@ -550,7 +555,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
550555
if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
551556
// try to move, don't replace:
552557
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
553-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
558+
longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
554559
} else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) {
555560
// replace atomically (impossible):
556561
assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING);
@@ -569,7 +574,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
569574
Files.delete(ciphertextTargetDir);
570575
}
571576
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
572-
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
577+
longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
573578
}
574579
dirIdProvider.move(ciphertextSourceDirFile, ciphertextTargetDirFile);
575580
cryptoPathMapper.invalidatePathMapping(cleartextSource);

src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,19 @@
1313
import com.google.common.cache.CacheLoader;
1414
import com.google.common.cache.LoadingCache;
1515
import com.google.common.io.BaseEncoding;
16-
import com.google.common.util.concurrent.UncheckedExecutionException;
1716
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
1817

1918
import javax.inject.Inject;
2019
import java.io.IOException;
2120
import java.io.UncheckedIOException;
22-
import java.nio.ByteBuffer;
2321
import java.nio.channels.WritableByteChannel;
2422
import java.nio.file.FileAlreadyExistsException;
2523
import java.nio.file.Files;
2624
import java.nio.file.Path;
2725
import java.nio.file.StandardOpenOption;
2826
import java.time.Duration;
2927
import java.util.Arrays;
28+
import java.util.Optional;
3029
import java.util.concurrent.ExecutionException;
3130

3231
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -86,34 +85,51 @@ public String deflate(String longFileName) {
8685
return shortName;
8786
}
8887

89-
public void persistCachedIfDeflated(Path ciphertextFile) throws IOException {
90-
String filename = ciphertextFile.getFileName().toString();
91-
if (isDeflated(filename)) {
92-
persistCached(filename);
93-
}
88+
private Path resolveMetadataFile(String shortName) {
89+
return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName);
9490
}
9591

96-
// visible for testing
97-
void persistCached(String shortName) throws IOException {
98-
readonlyFlag.assertWritable();
92+
public Optional<DeflatedFileName> getCached(Path ciphertextFile) {
93+
String shortName = ciphertextFile.getFileName().toString();
9994
String longName = longNames.getIfPresent(shortName);
100-
if (longName == null) {
101-
throw new IllegalStateException("Long name for " + shortName + " has not been shortened within the last " + MAX_CACHE_AGE);
102-
}
103-
Path file = resolveMetadataFile(shortName);
104-
Path fileDir = file.getParent();
105-
assert fileDir != null : "resolveMetadataFile returned path to a file";
106-
Files.createDirectories(fileDir);
107-
try (WritableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
108-
ch.write(UTF_8.encode(longName));
109-
} catch (FileAlreadyExistsException e) {
110-
// no-op: if the file already exists, we assume its content to be what we want (or we found a SHA1 collision ;-))
111-
assert Arrays.equals(Files.readAllBytes(file), longName.getBytes(UTF_8));
95+
if (longName != null) {
96+
return Optional.of(new DeflatedFileName(shortName, longName));
97+
} else {
98+
return Optional.empty();
11299
}
113100
}
114101

115-
private Path resolveMetadataFile(String shortName) {
116-
return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName);
102+
public class DeflatedFileName {
103+
104+
public final String shortName;
105+
public final String longName;
106+
107+
private DeflatedFileName(String shortName, String longName) {
108+
this.shortName = shortName;
109+
this.longName = longName;
110+
}
111+
112+
public void persist() {
113+
readonlyFlag.assertWritable();
114+
try {
115+
persistInternal();
116+
} catch (IOException e) {
117+
throw new UncheckedIOException(e);
118+
}
119+
}
120+
121+
private void persistInternal() throws IOException {
122+
Path file = resolveMetadataFile(shortName);
123+
Path fileDir = file.getParent();
124+
assert fileDir != null : "resolveMetadataFile returned path to a file";
125+
Files.createDirectories(fileDir);
126+
try (WritableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
127+
ch.write(UTF_8.encode(longName));
128+
} catch (FileAlreadyExistsException e) {
129+
// no-op: if the file already exists, we assume its content to be what we want (or we found a SHA1 collision ;-))
130+
assert Arrays.equals(Files.readAllBytes(file), longName.getBytes(UTF_8));
131+
}
132+
}
117133
}
118134

119135
}

src/main/java/org/cryptomator/cryptofs/Symlinks.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib
4343
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag);
4444
ByteBuffer content = UTF_8.encode(target.toString());
4545
openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content);
46-
longFileNameProvider.persistCachedIfDeflated(ciphertextSymlinkFile);
46+
longFileNameProvider.getCached(ciphertextSymlinkFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
4747
}
4848

4949
public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException {

src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public CryptoBasicFileAttributes(BasicFileAttributes delegate, CiphertextFileTyp
5454
}
5555

5656
private static long getPlaintextFileSize(Path ciphertextPath, long size, Optional<OpenCryptoFile> openCryptoFile, Cryptor cryptor) {
57-
return openCryptoFile.map(OpenCryptoFile::size).orElseGet(() -> calculatePlaintextFileSize(ciphertextPath, size, cryptor));
57+
return openCryptoFile.flatMap(OpenCryptoFile::size).orElseGet(() -> calculatePlaintextFileSize(ciphertextPath, size, cryptor));
5858
}
5959

6060
private static long calculatePlaintextFileSize(Path ciphertextPath, long size, Cryptor cryptor) {

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,38 +227,58 @@ public MappedByteBuffer map(MapMode mode, long position, long size) {
227227
}
228228

229229
@Override
230-
public FileLock lock(long position, long size, boolean shared) throws IOException {
230+
public FileLock lock(long pos, long size, boolean shared) throws IOException {
231231
assertOpen();
232232
if (shared && !options.readable()) {
233233
throw new NonReadableChannelException(); // shared lock only available on readable channel
234234
} else if (!shared && !options.writable()) {
235235
throw new NonWritableChannelException(); // exclusive lock only available on writable channel
236236
}
237-
long firstChunk = position / cryptor.fileContentCryptor().cleartextChunkSize();
238-
long lastChunk = firstChunk + size / cryptor.fileContentCryptor().cleartextChunkSize();
239-
long ciphertextPosition = cryptor.fileHeaderCryptor().headerSize() + firstChunk * cryptor.fileContentCryptor().ciphertextChunkSize();
240-
long ciphertextSize = (lastChunk - firstChunk + 1) * cryptor.fileContentCryptor().ciphertextChunkSize();
241-
FileLock ciphertextLock = ciphertextFileChannel.lock(ciphertextPosition, ciphertextSize, shared);
242-
return new CleartextFileLock(this, ciphertextLock, position, size);
237+
long beginOfFirstChunk = beginOfChunk(pos);
238+
long beginOfLastChunk = beginOfChunk(pos + size);
239+
final FileLock ciphertextLock;
240+
if (beginOfFirstChunk == Long.MAX_VALUE || beginOfLastChunk == Long.MAX_VALUE) {
241+
ciphertextLock = ciphertextFileChannel.lock(0l, Long.MAX_VALUE, shared);
242+
} else {
243+
long endOfLastChunk = beginOfLastChunk + cryptor.fileContentCryptor().ciphertextChunkSize();
244+
ciphertextLock = ciphertextFileChannel.lock(beginOfFirstChunk, endOfLastChunk - beginOfFirstChunk, shared);
245+
}
246+
return new CleartextFileLock(this, ciphertextLock, pos, size);
243247
}
244248

245249
@Override
246-
public FileLock tryLock(long position, long size, boolean shared) throws IOException {
250+
public FileLock tryLock(long pos, long size, boolean shared) throws IOException {
247251
assertOpen();
248252
if (shared && !options.readable()) {
249253
throw new NonReadableChannelException(); // shared lock only available on readable channel
250254
} else if (!shared && !options.writable()) {
251255
throw new NonWritableChannelException(); // exclusive lock only available on writable channel
252256
}
253-
long firstChunk = position / cryptor.fileContentCryptor().cleartextChunkSize();
254-
long lastChunk = firstChunk + size / cryptor.fileContentCryptor().cleartextChunkSize();
255-
long ciphertextPosition = cryptor.fileHeaderCryptor().headerSize() + firstChunk * cryptor.fileContentCryptor().ciphertextChunkSize();
256-
long ciphertextSize = (lastChunk - firstChunk + 1) * cryptor.fileContentCryptor().ciphertextChunkSize();
257-
FileLock ciphertextLock = ciphertextFileChannel.tryLock(ciphertextPosition, ciphertextSize, shared);
257+
long beginOfFirstChunk = beginOfChunk(pos);
258+
long beginOfLastChunk = beginOfChunk(pos + size);
259+
final FileLock ciphertextLock;
260+
if (beginOfFirstChunk == Long.MAX_VALUE || beginOfLastChunk == Long.MAX_VALUE) {
261+
ciphertextLock = ciphertextFileChannel.tryLock(0l, Long.MAX_VALUE, shared);
262+
} else {
263+
long endOfLastChunk = beginOfLastChunk + cryptor.fileContentCryptor().ciphertextChunkSize();
264+
ciphertextLock = ciphertextFileChannel.tryLock(beginOfFirstChunk, endOfLastChunk - beginOfFirstChunk, shared);
265+
}
258266
if (ciphertextLock == null) {
259267
return null;
260268
} else {
261-
return new CleartextFileLock(this, ciphertextLock, position, size);
269+
return new CleartextFileLock(this, ciphertextLock, pos, size);
270+
}
271+
}
272+
273+
// visible for testing
274+
long beginOfChunk(long cleartextPos) {
275+
long maxCiphertextPayloadSize = Long.MAX_VALUE - cryptor.fileHeaderCryptor().headerSize();
276+
long maxChunks = maxCiphertextPayloadSize / cryptor.fileContentCryptor().ciphertextChunkSize();
277+
long chunk = cleartextPos / cryptor.fileContentCryptor().cleartextChunkSize();
278+
if (chunk > maxChunks) {
279+
return Long.MAX_VALUE;
280+
} else {
281+
return chunk * cryptor.fileContentCryptor().ciphertextChunkSize() + cryptor.fileHeaderCryptor().headerSize();
262282
}
263283
}
264284

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.file.Path;
2626
import java.nio.file.attribute.FileTime;
2727
import java.time.Instant;
28+
import java.util.Optional;
2829
import java.util.concurrent.ConcurrentHashMap;
2930
import java.util.concurrent.ConcurrentMap;
3031
import java.util.concurrent.atomic.AtomicLong;
@@ -146,12 +147,15 @@ private void initFileSize(FileChannel ciphertextFileChannel) throws IOException
146147
}
147148

148149
/**
149-
* @return The size of the opened file
150-
* @throws IllegalStateException If the OpenCryptoFile {@link OpenCryptoFiles#getOrCreate(Path) has been created} without {@link #newFileChannel(EffectiveOpenOptions) creating a file channel} next.
150+
* @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.
151151
*/
152-
public long size() {
153-
Preconditions.checkState(fileSize.get() != -1l, "size must only be called after a FileChannel is created for this OpenCryptoFile");
154-
return fileSize.get();
152+
public Optional<Long> size() {
153+
long val = fileSize.get();
154+
if (val == -1l) {
155+
return Optional.empty();
156+
} else {
157+
return Optional.of(val);
158+
}
155159
}
156160

157161
public FileTime getLastModifiedTime() {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.io.IOException;
2727
import java.nio.ByteBuffer;
2828
import java.nio.channels.FileChannel;
29+
import java.nio.channels.FileLock;
2930
import java.nio.charset.StandardCharsets;
3031
import java.nio.file.FileSystem;
3132
import java.nio.file.Files;
@@ -110,6 +111,24 @@ public void afterEach() throws IOException {
110111
Files.deleteIfExists(file);
111112
}
112113

114+
@Test
115+
public void testLockEmptyChannel() throws IOException {
116+
try (FileChannel ch = FileChannel.open(file, CREATE, WRITE)) {
117+
try (FileLock lock = ch.lock()) {
118+
Assertions.assertNotNull(lock);
119+
}
120+
}
121+
}
122+
123+
@Test
124+
public void testTryLockEmptyChannel() throws IOException {
125+
try (FileChannel ch = FileChannel.open(file, CREATE, WRITE)) {
126+
try (FileLock lock = ch.tryLock()) {
127+
Assertions.assertNotNull(lock);
128+
}
129+
}
130+
}
131+
113132
// tests https://github.com/cryptomator/cryptofs/issues/55
114133
@Test
115134
public void testCreateNewFileSetsLastModifiedToNow() throws IOException, InterruptedException {

0 commit comments

Comments
 (0)