Skip to content

Commit

Permalink
Merge branch 'release/1.8.6'
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Jul 10, 2019
2 parents 8b9d605 + c493ebb commit fa335fa
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 66 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
<version>1.8.5</version>
<version>1.8.6</version>
<name>Cryptomator Crypto Filesystem</name>
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
<url>https://github.com/cryptomator/cryptofs</url>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
}
LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
longFileNameProvider.persistCachedIfDeflated(resolved);
longFileNameProvider.getCached(resolved).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
return resolved;
} catch (AuthenticationFailedException e) {
// not decryptable, no need to resolve any kind of conflict
Expand Down
23 changes: 14 additions & 9 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -304,7 +305,7 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
// create dir if and only if the dirFile has been created right now (not if it has been created before):
try {
Files.createDirectories(ciphertextDir.path);
longFileNameProvider.persistCachedIfDeflated(ciphertextDirFile);
longFileNameProvider.getCached(ciphertextDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
} catch (IOException e) {
// make sure there is no orphan dir file:
Files.delete(ciphertextDirFile);
Expand Down Expand Up @@ -355,7 +356,7 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp
} else {
// might also throw FileAlreadyExists:
FileChannel ch = openCryptoFiles.getOrCreate(ciphertextPath).newFileChannel(options);
longFileNameProvider.persistCachedIfDeflated(ciphertextPath);
longFileNameProvider.getCached(ciphertextPath).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
return ch;
}
}
Expand Down Expand Up @@ -423,8 +424,10 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.SYMLINK);
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new);
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile);
Files.copy(ciphertextSourceFile, ciphertextTargetFile, resolvedOptions);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);

} else {
CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource);
CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget);
Expand All @@ -436,17 +439,19 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE);
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile);
Files.copy(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);
}

private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
// DIRECTORY (non-recursive as per contract):
Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.DIRECTORY);
if (Files.notExists(ciphertextTargetDirFile)) {
// create new:
Optional<LongFileNameProvider.DeflatedFileName> deflatedFileName = longFileNameProvider.getCached(ciphertextTargetDirFile);
createDirectory(cleartextTarget);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist);
} else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
// keep existing (if empty):
Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
Expand Down Expand Up @@ -525,7 +530,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
twoPhaseMove.commit();
}
}
Expand All @@ -537,7 +542,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE);
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) {
Files.move(ciphertextSourceFile, ciphertextTargetFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetFile);
longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
twoPhaseMove.commit();
}
}
Expand All @@ -550,7 +555,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
// try to move, don't replace:
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
} else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) {
// replace atomically (impossible):
assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING);
Expand All @@ -569,7 +574,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
Files.delete(ciphertextTargetDir);
}
Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options);
longFileNameProvider.persistCachedIfDeflated(ciphertextTargetDirFile);
longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
}
dirIdProvider.move(ciphertextSourceDirFile, ciphertextTargetDirFile);
cryptoPathMapper.invalidatePathMapping(cleartextSource);
Expand Down
64 changes: 40 additions & 24 deletions src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.UncheckedExecutionException;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;

import javax.inject.Inject;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutionException;

import static java.nio.charset.StandardCharsets.UTF_8;
Expand Down Expand Up @@ -86,34 +85,51 @@ public String deflate(String longFileName) {
return shortName;
}

public void persistCachedIfDeflated(Path ciphertextFile) throws IOException {
String filename = ciphertextFile.getFileName().toString();
if (isDeflated(filename)) {
persistCached(filename);
}
private Path resolveMetadataFile(String shortName) {
return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName);
}

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

private Path resolveMetadataFile(String shortName) {
return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName);
public class DeflatedFileName {

public final String shortName;
public final String longName;

private DeflatedFileName(String shortName, String longName) {
this.shortName = shortName;
this.longName = longName;
}

public void persist() {
readonlyFlag.assertWritable();
try {
persistInternal();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private void persistInternal() throws IOException {
Path file = resolveMetadataFile(shortName);
Path fileDir = file.getParent();
assert fileDir != null : "resolveMetadataFile returned path to a file";
Files.createDirectories(fileDir);
try (WritableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
ch.write(UTF_8.encode(longName));
} catch (FileAlreadyExistsException e) {
// no-op: if the file already exists, we assume its content to be what we want (or we found a SHA1 collision ;-))
assert Arrays.equals(Files.readAllBytes(file), longName.getBytes(UTF_8));
}
}
}

}
2 changes: 1 addition & 1 deletion src/main/java/org/cryptomator/cryptofs/Symlinks.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag);
ByteBuffer content = UTF_8.encode(target.toString());
openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content);
longFileNameProvider.persistCachedIfDeflated(ciphertextSymlinkFile);
longFileNameProvider.getCached(ciphertextSymlinkFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist);
}

public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public CryptoBasicFileAttributes(BasicFileAttributes delegate, CiphertextFileTyp
}

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

private static long calculatePlaintextFileSize(Path ciphertextPath, long size, Cryptor cryptor) {
Expand Down
48 changes: 34 additions & 14 deletions src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,38 +227,58 @@ public MappedByteBuffer map(MapMode mode, long position, long size) {
}

@Override
public FileLock lock(long position, long size, boolean shared) throws IOException {
public FileLock lock(long pos, long size, boolean shared) throws IOException {
assertOpen();
if (shared && !options.readable()) {
throw new NonReadableChannelException(); // shared lock only available on readable channel
} else if (!shared && !options.writable()) {
throw new NonWritableChannelException(); // exclusive lock only available on writable channel
}
long firstChunk = position / cryptor.fileContentCryptor().cleartextChunkSize();
long lastChunk = firstChunk + size / cryptor.fileContentCryptor().cleartextChunkSize();
long ciphertextPosition = cryptor.fileHeaderCryptor().headerSize() + firstChunk * cryptor.fileContentCryptor().ciphertextChunkSize();
long ciphertextSize = (lastChunk - firstChunk + 1) * cryptor.fileContentCryptor().ciphertextChunkSize();
FileLock ciphertextLock = ciphertextFileChannel.lock(ciphertextPosition, ciphertextSize, shared);
return new CleartextFileLock(this, ciphertextLock, position, size);
long beginOfFirstChunk = beginOfChunk(pos);
long beginOfLastChunk = beginOfChunk(pos + size);
final FileLock ciphertextLock;
if (beginOfFirstChunk == Long.MAX_VALUE || beginOfLastChunk == Long.MAX_VALUE) {
ciphertextLock = ciphertextFileChannel.lock(0l, Long.MAX_VALUE, shared);
} else {
long endOfLastChunk = beginOfLastChunk + cryptor.fileContentCryptor().ciphertextChunkSize();
ciphertextLock = ciphertextFileChannel.lock(beginOfFirstChunk, endOfLastChunk - beginOfFirstChunk, shared);
}
return new CleartextFileLock(this, ciphertextLock, pos, size);
}

@Override
public FileLock tryLock(long position, long size, boolean shared) throws IOException {
public FileLock tryLock(long pos, long size, boolean shared) throws IOException {
assertOpen();
if (shared && !options.readable()) {
throw new NonReadableChannelException(); // shared lock only available on readable channel
} else if (!shared && !options.writable()) {
throw new NonWritableChannelException(); // exclusive lock only available on writable channel
}
long firstChunk = position / cryptor.fileContentCryptor().cleartextChunkSize();
long lastChunk = firstChunk + size / cryptor.fileContentCryptor().cleartextChunkSize();
long ciphertextPosition = cryptor.fileHeaderCryptor().headerSize() + firstChunk * cryptor.fileContentCryptor().ciphertextChunkSize();
long ciphertextSize = (lastChunk - firstChunk + 1) * cryptor.fileContentCryptor().ciphertextChunkSize();
FileLock ciphertextLock = ciphertextFileChannel.tryLock(ciphertextPosition, ciphertextSize, shared);
long beginOfFirstChunk = beginOfChunk(pos);
long beginOfLastChunk = beginOfChunk(pos + size);
final FileLock ciphertextLock;
if (beginOfFirstChunk == Long.MAX_VALUE || beginOfLastChunk == Long.MAX_VALUE) {
ciphertextLock = ciphertextFileChannel.tryLock(0l, Long.MAX_VALUE, shared);
} else {
long endOfLastChunk = beginOfLastChunk + cryptor.fileContentCryptor().ciphertextChunkSize();
ciphertextLock = ciphertextFileChannel.tryLock(beginOfFirstChunk, endOfLastChunk - beginOfFirstChunk, shared);
}
if (ciphertextLock == null) {
return null;
} else {
return new CleartextFileLock(this, ciphertextLock, position, size);
return new CleartextFileLock(this, ciphertextLock, pos, size);
}
}

// visible for testing
long beginOfChunk(long cleartextPos) {
long maxCiphertextPayloadSize = Long.MAX_VALUE - cryptor.fileHeaderCryptor().headerSize();
long maxChunks = maxCiphertextPayloadSize / cryptor.fileContentCryptor().ciphertextChunkSize();
long chunk = cleartextPos / cryptor.fileContentCryptor().cleartextChunkSize();
if (chunk > maxChunks) {
return Long.MAX_VALUE;
} else {
return chunk * cryptor.fileContentCryptor().ciphertextChunkSize() + cryptor.fileHeaderCryptor().headerSize();
}
}

Expand Down
14 changes: 9 additions & 5 deletions src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -146,12 +147,15 @@ private void initFileSize(FileChannel ciphertextFileChannel) throws IOException
}

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

public FileTime getLastModifiedTime() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
Expand Down Expand Up @@ -110,6 +111,24 @@ public void afterEach() throws IOException {
Files.deleteIfExists(file);
}

@Test
public void testLockEmptyChannel() throws IOException {
try (FileChannel ch = FileChannel.open(file, CREATE, WRITE)) {
try (FileLock lock = ch.lock()) {
Assertions.assertNotNull(lock);
}
}
}

@Test
public void testTryLockEmptyChannel() throws IOException {
try (FileChannel ch = FileChannel.open(file, CREATE, WRITE)) {
try (FileLock lock = ch.tryLock()) {
Assertions.assertNotNull(lock);
}
}
}

// tests https://github.com/cryptomator/cryptofs/issues/55
@Test
public void testCreateNewFileSetsLastModifiedToNow() throws IOException, InterruptedException {
Expand Down
Loading

0 comments on commit fa335fa

Please sign in to comment.