Skip to content

Commit 000dc40

Browse files
committed
Merge branch 'release/1.9.2'
2 parents 54149b5 + a251ad2 commit 000dc40

File tree

10 files changed

+233
-19
lines changed

10 files changed

+233
-19
lines changed

pom.xml

Lines changed: 28 additions & 3 deletions
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.9.1</version>
5+
<version>1.9.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>
@@ -15,11 +15,11 @@
1515

1616
<properties>
1717
<cryptolib.version>1.3.0</cryptolib.version>
18-
<dagger.version>2.25.4</dagger.version>
18+
<dagger.version>2.26</dagger.version>
1919
<guava.version>28.2-jre</guava.version>
2020
<slf4j.version>1.7.30</slf4j.version>
2121

22-
<junit.jupiter.version>5.5.2</junit.jupiter.version>
22+
<junit.jupiter.version>5.6.0</junit.jupiter.version>
2323
<mockito.version>3.2.4</mockito.version>
2424
<hamcrest.version>2.2</hamcrest.version>
2525
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -239,6 +239,31 @@
239239
</execution>
240240
</executions>
241241
<configuration>
242+
<tags>
243+
<!-- workaround for "unknown tag: implNote", see https://blog.codefx.org/java/new-javadoc-tags/#Maven -->
244+
<tag>
245+
<name>apiNote</name>
246+
<placement>a</placement>
247+
<head>API Note:</head>
248+
</tag>
249+
<tag>
250+
<name>implSpec</name>
251+
<placement>a</placement>
252+
<head>Implementation Requirements:</head>
253+
</tag>
254+
<tag>
255+
<name>implNote</name>
256+
<placement>a</placement>
257+
<head>Implementation Note:</head>
258+
</tag>
259+
<tag><name>param</name></tag>
260+
<tag><name>return</name></tag>
261+
<tag><name>throws</name></tag>
262+
<tag><name>since</name></tag>
263+
<tag><name>version</name></tag>
264+
<tag><name>serialData</name></tag>
265+
<tag><name>see</name></tag>
266+
</tags>
242267
<additionalDependencies>
243268
<additionalDependency>
244269
<groupId>javax.annotation</groupId>

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
1212
import org.cryptomator.cryptofs.common.Constants;
13+
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
1314
import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
1415
import org.cryptomator.cryptofs.migration.Migrators;
1516
import org.cryptomator.cryptolib.Cryptors;
@@ -124,6 +125,7 @@ private static SecureRandom strongSecureRandom() {
124125
* @param properties Parameters used during initialization of the file system
125126
* @return a new file system
126127
* @throws FileSystemNeedsMigrationException if the vault format needs to get updated and <code>properties</code> did not contain a flag for implicit migration.
128+
* @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
127129
* @throws IOException if an I/O error occurs creating the file system
128130
*/
129131
public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException {
@@ -138,6 +140,7 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
138140
* @param masterkeyFilename Name of the masterkey file
139141
* @param passphrase Passphrase that should be used to unlock the vault
140142
* @throws NotDirectoryException If the given path is not an existing directory.
143+
* @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
141144
* @throws IOException If the vault structure could not be initialized due to I/O errors
142145
* @since 1.3.0
143146
*/
@@ -153,13 +156,15 @@ public static void initialize(Path pathToVault, String masterkeyFilename, CharSe
153156
* @param pepper Application-specific pepper used during key derivation
154157
* @param passphrase Passphrase that should be used to unlock the vault
155158
* @throws NotDirectoryException If the given path is not an existing directory.
159+
* @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
156160
* @throws IOException If the vault structure could not be initialized due to I/O errors
157161
* @since 1.3.2
158162
*/
159163
public static void initialize(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws NotDirectoryException, IOException {
160164
if (!Files.isDirectory(pathToVault)) {
161165
throw new NotDirectoryException(pathToVault.toString());
162166
}
167+
new FileSystemCapabilityChecker().checkCapabilities(pathToVault);
163168
try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) {
164169
// save masterkey file:
165170
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
@@ -288,6 +293,8 @@ public String getScheme() {
288293
public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) throws IOException {
289294
CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
290295
CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
296+
297+
new FileSystemCapabilityChecker().checkCapabilities(parsedUri.pathToVault());
291298

292299
// TODO remove implicit initialization in 2.0.0
293300
initializeFileSystemIfRequired(parsedUri, properties);

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,7 @@ private void forceInternal(boolean metaData) throws IOException {
211211
flush();
212212
ciphertextFileChannel.force(metaData);
213213
if (metaData) {
214-
FileTime lastModifiedTime = isWritable() ? FileTime.from(lastModified.get()) : null;
215-
FileTime lastAccessTime = FileTime.from(Instant.now());
216-
attrViewProvider.get().setTimes(lastModifiedTime, lastAccessTime, null);
214+
persistLastModified();
217215
}
218216
}
219217

@@ -229,6 +227,17 @@ private void flush() throws IOException {
229227
}
230228
}
231229

230+
/**
231+
* Corrects the last modified and access date due to possible cache invalidation (i.e. write operation!)
232+
*
233+
* @throws IOException
234+
*/
235+
private void persistLastModified() throws IOException {
236+
FileTime lastModifiedTime = isWritable() ? FileTime.from(lastModified.get()) : null;
237+
FileTime lastAccessTime = FileTime.from(Instant.now());
238+
attrViewProvider.get().setTimes(lastModifiedTime, lastAccessTime, null);
239+
}
240+
232241
@Override
233242
public MappedByteBuffer map(MapMode mode, long position, long size) {
234243
throw new UnsupportedOperationException();
@@ -294,6 +303,7 @@ long beginOfChunk(long cleartextPos) {
294303
protected void implCloseChannel() throws IOException {
295304
try {
296305
flush();
306+
persistLastModified();
297307
} finally {
298308
super.implCloseChannel();
299309
closeListener.closed(this);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.cryptomator.cryptofs.common;
2+
3+
import com.google.common.base.Joiner;
4+
import com.google.common.base.Strings;
5+
import com.google.common.io.MoreFiles;
6+
import com.google.common.io.RecursiveDeleteOption;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.io.IOException;
11+
import java.nio.file.FileSystemException;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
15+
public class FileSystemCapabilityChecker {
16+
17+
private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
18+
19+
public enum Capability {
20+
/**
21+
* File system supports filenames with ≥ 230 chars.
22+
*/
23+
LONG_FILENAMES,
24+
25+
/**
26+
* File system supports paths with ≥ 400 chars.
27+
*/
28+
LONG_PATHS,
29+
}
30+
31+
/**
32+
* Checks whether the underlying filesystem has all required capabilities.
33+
*
34+
* @param pathToVault Path to a vault's storage location
35+
* @throws MissingCapabilityException if any check fails
36+
* @implNote Only short-running tests with constant time are performed
37+
* @since 1.9.2
38+
*/
39+
public void checkCapabilities(Path pathToVault) throws MissingCapabilityException {
40+
Path checkDir = pathToVault.resolve("c");
41+
try {
42+
checkLongFilenames(checkDir);
43+
checkLongFilePaths(checkDir);
44+
} finally {
45+
try {
46+
MoreFiles.deleteRecursively(checkDir, RecursiveDeleteOption.ALLOW_INSECURE);
47+
} catch (IOException e) {
48+
LOG.warn("Failed to clean up " + checkDir, e);
49+
}
50+
}
51+
}
52+
53+
private void checkLongFilenames(Path checkDir) throws MissingCapabilityException {
54+
String longFileName = Strings.repeat("a", 226) + ".c9r";
55+
Path p = checkDir.resolve(longFileName);
56+
try {
57+
Files.createDirectories(p);
58+
} catch (IOException e) {
59+
throw new MissingCapabilityException(p, Capability.LONG_FILENAMES);
60+
}
61+
}
62+
63+
private void checkLongFilePaths(Path checkDir) throws MissingCapabilityException {
64+
String longFileName = Strings.repeat("a", 96) + ".c9r";
65+
String longPath = Joiner.on('/').join(longFileName, longFileName, longFileName, longFileName);
66+
Path p = checkDir.resolve(longPath);
67+
try {
68+
Files.createDirectories(p);
69+
} catch (IOException e) {
70+
throw new MissingCapabilityException(p, Capability.LONG_PATHS);
71+
}
72+
}
73+
74+
public static class MissingCapabilityException extends FileSystemException {
75+
76+
private final Capability missingCapability;
77+
78+
public MissingCapabilityException(Path path, Capability missingCapability) {
79+
super(path.toString(), null, "Filesystem doesn't support " + missingCapability);
80+
this.missingCapability = missingCapability;
81+
}
82+
83+
public Capability getMissingCapability() {
84+
return missingCapability;
85+
}
86+
}
87+
88+
}

src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import dagger.Module;
1414
import dagger.Provides;
1515
import dagger.multibindings.IntoMap;
16+
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
1617
import org.cryptomator.cryptofs.migration.api.Migrator;
1718
import org.cryptomator.cryptofs.migration.v6.Version6Migrator;
1819
import org.cryptomator.cryptofs.migration.v7.Version7Migrator;
@@ -34,6 +35,11 @@ class MigrationModule {
3435
CryptorProvider provideVersion1CryptorProvider() {
3536
return version1Cryptor;
3637
}
38+
39+
@Provides
40+
FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
41+
return new FileSystemCapabilityChecker();
42+
}
3743

3844
@Provides
3945
@IntoMap

src/main/java/org/cryptomator/cryptofs/migration/Migrators.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import javax.inject.Inject;
1717

1818
import org.cryptomator.cryptofs.common.Constants;
19+
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
1920
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
2021
import org.cryptomator.cryptofs.migration.api.Migrator;
2122
import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
@@ -46,10 +47,12 @@ public class Migrators {
4647
.build();
4748

4849
private final Map<Migration, Migrator> migrators;
50+
private final FileSystemCapabilityChecker fsCapabilityChecker;
4951

5052
@Inject
51-
Migrators(Map<Migration, Migrator> migrators) {
53+
Migrators(Map<Migration, Migrator> migrators, FileSystemCapabilityChecker fsCapabilityChecker) {
5254
this.migrators = migrators;
55+
this.fsCapabilityChecker = fsCapabilityChecker;
5356
}
5457

5558
private static SecureRandom strongSecureRandom() {
@@ -87,9 +90,12 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws
8790
* @param passphrase The passphrase needed to unlock the vault
8891
* @throws NoApplicableMigratorException If the vault can not be migrated, because no migrator could be found
8992
* @throws InvalidPassphraseException If the passphrase could not be used to unlock the vault
93+
* @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
9094
* @throws IOException if an I/O error occurs migrating the vault
9195
*/
9296
public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
97+
fsCapabilityChecker.checkCapabilities(pathToVault);
98+
9399
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
94100
byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
95101
KeyFile keyFile = KeyFile.parse(keyFileContents);

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ public void setupClass(@TempDir Path tmpDir) throws IOException {
6363
fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withPassphrase("asd").build());
6464
}
6565

66-
// tests https://github.com/cryptomator/cryptofs/issues/56
66+
// tests https://github.com/cryptomator/cryptofs/issues/69
6767
@Test
68-
public void testForceDoesntBumpModifiedDate() throws IOException {
68+
public void testCloseDoesNotBumpModifiedDate() throws IOException {
6969
Path file = fileSystem.getPath("/file.txt");
7070

7171
Instant t0, t1;
@@ -76,7 +76,43 @@ public void testForceDoesntBumpModifiedDate() throws IOException {
7676
}
7777

7878
t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
79-
Assertions.assertTrue(t1.equals(t0));
79+
Assertions.assertEquals(t0, t1);
80+
}
81+
82+
@Test
83+
public void testLastModifiedIsPreservedOverSeveralOperations() throws IOException, InterruptedException {
84+
Path file = fileSystem.getPath("/file2.txt");
85+
86+
Instant t0, t1, t2, t3, t4, t5;
87+
t0 = Instant.ofEpochSecond(123456789).truncatedTo(ChronoUnit.SECONDS);
88+
ByteBuffer data = ByteBuffer.wrap("CryptoFS".getBytes());
89+
90+
try (FileChannel ch = FileChannel.open(file, CREATE_NEW, WRITE)) {
91+
t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
92+
Thread.currentThread().sleep(50);
93+
94+
ch.write(data);
95+
ch.force(true);
96+
Thread.currentThread().sleep(50);
97+
t2 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
98+
99+
Files.setLastModifiedTime(file, FileTime.from(t0));
100+
ch.force(true);
101+
Thread.currentThread().sleep(50);
102+
t3 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
103+
104+
ch.write(data);
105+
ch.force(true);
106+
Thread.currentThread().sleep(1000);
107+
t4 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
108+
109+
}
110+
111+
t5 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
112+
Assertions.assertNotEquals(t1, t2);
113+
Assertions.assertEquals(t0, t3);
114+
Assertions.assertNotEquals(t4, t3);
115+
Assertions.assertEquals(t4, t5);
80116
}
81117

82118
}

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,26 @@ public void testCloseTriggersCloseListener() throws IOException {
222222

223223
verify(closeListener).closed(inTest);
224224
}
225+
226+
@Test
227+
public void testCloseUpdatesLastModifiedTimeIfWriteable() throws IOException {
228+
when(options.writable()).thenReturn(true);
229+
lastModified.set(Instant.ofEpochMilli(123456789000l));
230+
FileTime fileTime = FileTime.from(lastModified.get());
231+
232+
inTest.implCloseChannel();
233+
234+
verify(attributeView).setTimes(Mockito.eq(fileTime), Mockito.any(), Mockito.isNull());
235+
}
236+
237+
@Test
238+
public void testCloseDoesNotUpdateLastModifiedTimeIfReadOnly() throws IOException {
239+
when(options.writable()).thenReturn(false);
240+
241+
inTest.implCloseChannel();
242+
243+
verify(attributeView).setTimes(Mockito.isNull(), Mockito.any(), Mockito.isNull());
244+
}
225245
}
226246

227247
@Test
@@ -267,7 +287,7 @@ public void testTryLockReturnsNullIfDelegateReturnsNull() throws IOException {
267287
@Test
268288
@DisplayName("successful tryLock()")
269289
public void testTryLockReturnsCryptoFileLockWrappingDelegate() throws IOException {
270-
when(ciphertextFileChannel.tryLock(380l, 4670l+110l-380l, true)).thenReturn(delegate);
290+
when(ciphertextFileChannel.tryLock(380l, 4670l + 110l - 380l, true)).thenReturn(delegate);
271291

272292
FileLock result = inTest.tryLock(372l, 3828l, true);
273293

@@ -283,7 +303,7 @@ public void testTryLockReturnsCryptoFileLockWrappingDelegate() throws IOExceptio
283303
@Test
284304
@DisplayName("successful lock()")
285305
public void testLockReturnsCryptoFileLockWrappingDelegate() throws IOException {
286-
when(ciphertextFileChannel.lock(380l, 4670l+110l-380l, true)).thenReturn(delegate);
306+
when(ciphertextFileChannel.lock(380l, 4670l + 110l - 380l, true)).thenReturn(delegate);
287307

288308
FileLock result = inTest.lock(372l, 3828l, true);
289309

@@ -472,7 +492,7 @@ public void testDontRewriteHeader() throws IOException {
472492
inTest = new CleartextFileChannel(ciphertextFileChannel, header, false, readWriteLock, cryptor, chunkCache, options, fileSize, lastModified, attributeViewSupplier, exceptionsDuringWrite, closeListener, stats);
473493

474494
inTest.force(true);
475-
495+
476496
Mockito.verify(ciphertextFileChannel, Mockito.never()).write(Mockito.any(), Mockito.eq(0l));
477497
}
478498

0 commit comments

Comments
 (0)