From 88ff7186d63a4354a8297f6c97e2357701a6ecfa Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 20 Aug 2019 13:35:53 +0200 Subject: [PATCH 01/62] Reapply changes from 3ce8537eb84f887d608c3e2d9396308898bccb71 --- src/main/java/org/cryptomator/cryptofs/Constants.java | 2 +- .../java/org/cryptomator/cryptofs/ConflictResolverTest.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index 3ab2b461..ed0a2e5f 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -15,7 +15,7 @@ public final class Constants { static final String DATA_DIR_NAME = "d"; static final String METADATA_DIR_NAME = "m"; - static final int SHORT_NAMES_MAX_LENGTH = 129; + static final int SHORT_NAMES_MAX_LENGTH = 254; // length of a ciphertext filename before it gets shortened. It should be less than 260 (Windows MAX_PATH limit) and a multiple of 8 when the file extensions (4) and prefix (2) is subtracted. static final String ROOT_DIR_ID = ""; static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 diff --git a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java index 5cbb3ab7..f480b8bf 100644 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs; +import com.google.common.base.Strings; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; @@ -130,7 +131,8 @@ public void testRenameNormalFile(String conflictingFileName) throws IOException @ParameterizedTest @ValueSource(strings = {"ABCDEF== (1).lng", "conflict_ABCDEF==.lng"}) public void testRenameLongFile(String conflictingFileName) throws IOException { - String longCiphertextName = "ABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGH2345===="; + String longCiphertextName = Strings.repeat("ABCDEFGH",Constants.SHORT_NAMES_MAX_LENGTH /8 + 1); + assert longCiphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH; Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); Mockito.when(longFileNameProvider.inflate("ABCDEF==.lng")).thenReturn("FEDCBA=="); From 5e49e5ab9c823e75410a4e879a124885c0ca32c9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 20 Aug 2019 16:05:20 +0200 Subject: [PATCH 02/62] Added tests for vault format 7 migration (references #64) --- .../org/cryptomator/cryptofs/Constants.java | 2 +- .../cryptofs/migration/Migration.java | 9 +- .../cryptofs/migration/MigrationModule.java | 8 + .../migration/v7/Version7Migrator.java | 152 ++++++++++++++++++ .../migration/v7/Version7MigratorTest.java | 110 +++++++++++++ 5 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index ed0a2e5f..dbd36383 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -10,7 +10,7 @@ public final class Constants { - public static final int VAULT_VERSION = 6; + public static final int VAULT_VERSION = 7; public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; static final String DATA_DIR_NAME = "d"; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java index 932506c8..c214ca2d 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java @@ -14,11 +14,16 @@ enum Migration { /** * Migrates vault format 5 to 6. */ - FIVE_TO_SIX(5); + FIVE_TO_SIX(5), + + /** + * Migrates vault format 5 to 6. + */ + SIX_TO_SEVEN(6); private final int applicableVersion; - private Migration(int applicableVersion) { + Migration(int applicableVersion) { this.applicableVersion = applicableVersion; } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java index d5a99ad8..41a83dd5 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java @@ -15,6 +15,7 @@ import dagger.multibindings.IntoMap; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.v6.Version6Migrator; +import org.cryptomator.cryptofs.migration.v7.Version7Migrator; import org.cryptomator.cryptolib.api.CryptorProvider; import static java.lang.annotation.ElementType.METHOD; @@ -41,6 +42,13 @@ Migrator provideVersion6Migrator(Version6Migrator migrator) { return migrator; } + @Provides + @IntoMap + @MigratorKey(Migration.SIX_TO_SEVEN) + Migrator provideVersion7Migrator(Version7Migrator migrator) { + return migrator; + } + // @Provides // @IntoMap // @MigratorKey(Migration.SIX_TO_SEVEN) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java new file mode 100644 index 00000000..cd3bb1ee --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE file. + *******************************************************************************/ +package org.cryptomator.cryptofs.migration.v7; + +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.BackupUtil; +import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.migration.api.Migrator; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.KeyFile; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class Version7Migrator implements Migrator { + + private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class); + + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private static final int FILENAME_BUFFER_SIZE = 10 * 1024; + private static final String NEW_SHORTENED_SUFFIX = ".c9s"; + private static final String NEW_NORMAL_SUFFIX = ".c9r"; + + private final CryptorProvider cryptorProvider; + + @Inject + public Version7Migrator(CryptorProvider cryptorProvider) { + this.cryptorProvider = cryptorProvider; + } + + @Override + public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot); + Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); + byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile); + KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); + try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) { + // create backup, as soon as we know the password was correct: + Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); + Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); + LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); + + Map namePairs = loadShortenedNames(vaultRoot); + migrateFileNames(namePairs, vaultRoot); + + // TODO remove deprecated .lng from /m/ + + // rewrite masterkey file with normalized passphrase: + byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 7).serialize(); + Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING); + LOG.info("Updated masterkey."); + } + LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot); + } + + /** + * With vault format 7 we increased the file shortening threshold. + * + * @param vaultRoot + * @return + * @throws IOException + */ + // visible for testing + Map loadShortenedNames(Path vaultRoot) throws IOException { + Path metadataDir = vaultRoot.resolve("m"); + Map result = new HashMap<>(); + ByteBuffer longNameBuffer = ByteBuffer.allocate(FILENAME_BUFFER_SIZE); + Files.walkFileTree(metadataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + try (SeekableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.READ)) { + if (ch.size() > longNameBuffer.capacity()) { + LOG.error("Migrator not suited to handle filenames as large as {}. Aborting without changes.", ch.size()); + throw new IOException("Filename too large for migration: " + file); + } else { + longNameBuffer.clear(); + ch.read(longNameBuffer); + longNameBuffer.flip(); + String longName = UTF_8.decode(longNameBuffer).toString(); + String shortName = file.getFileName().toString(); + result.put(shortName, longName); + return FileVisitResult.CONTINUE; + } + } + } + }); + return result; + } + + void migrateFileNames(Map deflatedNames, Path vaultRoot) throws IOException { + Path dataDir = vaultRoot.resolve("d"); + Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path oldfile, BasicFileAttributes attrs) throws IOException { + String oldfilename = oldfile.getFileName().toString(); + String newfilename; + if (deflatedNames.containsKey(oldfilename)) { + newfilename = deflatedNames.get(oldfilename) + NEW_NORMAL_SUFFIX; + } else { + newfilename = oldfilename + NEW_NORMAL_SUFFIX; + } + + if (newfilename.length() > 254) { // Value of Constants#SHORT_NAMES_MAX_LENGTH as in Vault Format 7 + newfilename = deflate(vaultRoot, newfilename); + } + + Path newfile = oldfile.resolveSibling(newfilename); + LOG.info("RENAME {} TO {}", oldfile, newfile); + return FileVisitResult.CONTINUE; + } + }); + } + + String deflate(Path vaultRoot, String longName) throws IOException { + Path metadataDir = vaultRoot.resolve("m"); + byte[] longFileNameBytes = longName.getBytes(UTF_8); + byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); + String shortName = BASE32.encode(hash) + NEW_SHORTENED_SUFFIX; + Path metadataFile = metadataDir.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName); + LOG.info("CREATE {}", metadataFile); + Files.createDirectories(metadataFile.getParent()); + Files.write(metadataFile, shortName.getBytes(UTF_8)); + return shortName; + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java new file mode 100644 index 00000000..becf94ac --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -0,0 +1,110 @@ +package org.cryptomator.cryptofs.migration.v7; + +import com.google.common.base.Strings; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import org.cryptomator.cryptofs.migration.api.Migrator; +import org.cryptomator.cryptofs.mocks.NullSecureRandom; +import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.CryptorProvider; +import org.cryptomator.cryptolib.api.KeyFile; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +public class Version7MigratorTest { + + private FileSystem fs; + private Path vaultRoot; + private Path dataDir; + private Path metaDir; + private Path masterkeyFile; + private CryptorProvider cryptorProvider; + + @BeforeEach + public void setup() throws IOException { + cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE); + fs = Jimfs.newFileSystem(Configuration.unix()); + vaultRoot = fs.getPath("/vaultDir"); + dataDir = vaultRoot.resolve("d"); + metaDir = vaultRoot.resolve("m"); + masterkeyFile = vaultRoot.resolve("masterkey.cryptomator"); + Files.createDirectory(vaultRoot); + Files.createDirectory(dataDir); + Files.createDirectory(metaDir); + try (Cryptor cryptor = cryptorProvider.createNew()) { + KeyFile keyFile = cryptor.writeKeysToMasterkeyFile("test", 6); + Files.write(masterkeyFile, keyFile.serialize()); + } + Path dataFile1 = dataDir.resolve("00/000000000000000000000000000000/111"); + Path dataFile2 = dataDir.resolve("00/000000000000000000000000000000/1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng"); // 222 + Path metaFile2 = metaDir.resolve("1c/66/1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng"); + Path dataFile3 = dataDir.resolve("00/000000000000000000000000000000/0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng"); // 129 chars 33333... + Path metaFile3 = metaDir.resolve("0b/51/0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng"); + Path dataFile4 = dataDir.resolve("00/000000000000000000000000000000/caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng"); // 130 chars 44444... + Path metaFile4 = metaDir.resolve("ca/f8/caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng"); + Path dataFile5 = dataDir.resolve("00/000000000000000000000000000000/1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng"); // 250 chars 55555... + Path metaFile5 = metaDir.resolve("1c/b1/1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng"); + Path dataFile6 = dataDir.resolve("00/000000000000000000000000000000/c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng"); // 251 chars 66666... + Path metaFile6 = metaDir.resolve("c7/d6/c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng"); + Files.createDirectories(dataDir.resolve("00/000000000000000000000000000000")); + Files.createFile(dataFile1); + Files.createFile(dataFile2); + Files.createFile(dataFile3); + Files.createFile(dataFile4); + Files.createFile(dataFile5); + Files.createFile(dataFile6); + Files.createDirectories(metaFile2.getParent()); + Files.createDirectories(metaFile3.getParent()); + Files.createDirectories(metaFile4.getParent()); + Files.createDirectories(metaFile5.getParent()); + Files.createDirectories(metaFile6.getParent()); + Files.write(metaFile2, Strings.repeat("2", 3).getBytes(StandardCharsets.UTF_8)); + Files.write(metaFile3, Strings.repeat("3", 129).getBytes(StandardCharsets.UTF_8)); + Files.write(metaFile4, Strings.repeat("4", 130).getBytes(StandardCharsets.UTF_8)); + Files.write(metaFile5, Strings.repeat("5", 250).getBytes(StandardCharsets.UTF_8)); + Files.write(metaFile6, Strings.repeat("6", 251).getBytes(StandardCharsets.UTF_8)); + } + + @AfterEach + public void teardown() throws IOException { + fs.close(); + } + + @Test + public void testLoadShortenedNames() throws IOException { + Version7Migrator migrator = new Version7Migrator(cryptorProvider); + + Map namePairs = migrator.loadShortenedNames(vaultRoot); + + // <= 254 chars should be unshortened: + Assertions.assertEquals(Strings.repeat("2", 3), namePairs.get("1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng")); + Assertions.assertEquals(Strings.repeat("3", 129), namePairs.get("0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng")); + Assertions.assertEquals(Strings.repeat("4", 130), namePairs.get("caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng")); + Assertions.assertEquals(Strings.repeat("5", 250), namePairs.get("1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng")); + // > 254 chars should remain shortened: + Assertions.assertFalse(namePairs.containsKey("c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng")); + } + + @Test + public void testMigration() throws IOException { + KeyFile beforeMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); + Assertions.assertEquals(6, beforeMigration.getVersion()); + + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); + Assertions.assertEquals(7, afterMigration.getVersion()); + } + +} From 92dd9d5eaea3aa208d4d4ffbc80ffcce5dce25f3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 20 Aug 2019 17:36:05 +0200 Subject: [PATCH 03/62] fixed unit test --- .../cryptofs/migration/v7/Version7MigratorTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index becf94ac..95a146f6 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -91,8 +91,7 @@ public void testLoadShortenedNames() throws IOException { Assertions.assertEquals(Strings.repeat("3", 129), namePairs.get("0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng")); Assertions.assertEquals(Strings.repeat("4", 130), namePairs.get("caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng")); Assertions.assertEquals(Strings.repeat("5", 250), namePairs.get("1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng")); - // > 254 chars should remain shortened: - Assertions.assertFalse(namePairs.containsKey("c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng")); + Assertions.assertEquals(Strings.repeat("6", 251), namePairs.get("c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng")); } @Test From cdb50eb2c4132d396512f24dd25bf634ecd90b4b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 20 Aug 2019 17:47:59 +0200 Subject: [PATCH 04/62] renamed test files to upper case base32 --- .../migration/v7/Version7MigratorTest.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index 95a146f6..ceb0c2af 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -46,16 +46,16 @@ public void setup() throws IOException { Files.write(masterkeyFile, keyFile.serialize()); } Path dataFile1 = dataDir.resolve("00/000000000000000000000000000000/111"); - Path dataFile2 = dataDir.resolve("00/000000000000000000000000000000/1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng"); // 222 - Path metaFile2 = metaDir.resolve("1c/66/1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng"); - Path dataFile3 = dataDir.resolve("00/000000000000000000000000000000/0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng"); // 129 chars 33333... - Path metaFile3 = metaDir.resolve("0b/51/0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng"); - Path dataFile4 = dataDir.resolve("00/000000000000000000000000000000/caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng"); // 130 chars 44444... - Path metaFile4 = metaDir.resolve("ca/f8/caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng"); - Path dataFile5 = dataDir.resolve("00/000000000000000000000000000000/1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng"); // 250 chars 55555... - Path metaFile5 = metaDir.resolve("1c/b1/1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng"); - Path dataFile6 = dataDir.resolve("00/000000000000000000000000000000/c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng"); // 251 chars 66666... - Path metaFile6 = metaDir.resolve("c7/d6/c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng"); + Path dataFile2 = dataDir.resolve("00/000000000000000000000000000000/1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng"); // 222 + Path metaFile2 = metaDir.resolve("1C/66/1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng"); + Path dataFile3 = dataDir.resolve("00/000000000000000000000000000000/0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng"); // 129 chars 33333... + Path metaFile3 = metaDir.resolve("0B/51/0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng"); + Path dataFile4 = dataDir.resolve("00/000000000000000000000000000000/CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng"); // 130 chars 44444... + Path metaFile4 = metaDir.resolve("CA/F8/CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng"); + Path dataFile5 = dataDir.resolve("00/000000000000000000000000000000/1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng"); // 250 chars 55555... + Path metaFile5 = metaDir.resolve("1C/B1/1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng"); + Path dataFile6 = dataDir.resolve("00/000000000000000000000000000000/C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng"); // 251 chars 66666... + Path metaFile6 = metaDir.resolve("C7/D6/C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng"); Files.createDirectories(dataDir.resolve("00/000000000000000000000000000000")); Files.createFile(dataFile1); Files.createFile(dataFile2); @@ -87,11 +87,11 @@ public void testLoadShortenedNames() throws IOException { Map namePairs = migrator.loadShortenedNames(vaultRoot); // <= 254 chars should be unshortened: - Assertions.assertEquals(Strings.repeat("2", 3), namePairs.get("1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9.lng")); - Assertions.assertEquals(Strings.repeat("3", 129), namePairs.get("0b51fc45f30a0c3027f2b4c4698c5efca3c62fe0.lng")); - Assertions.assertEquals(Strings.repeat("4", 130), namePairs.get("caf8f7708cbf2fd3e735a0c765ebb8e0b879360a.lng")); - Assertions.assertEquals(Strings.repeat("5", 250), namePairs.get("1cb1308d10cf786a827c91eec1ff7b08d91acad6.lng")); - Assertions.assertEquals(Strings.repeat("6", 251), namePairs.get("c7d6c6201b5344583a9ed2d8f5c3239ccf666230.lng")); + Assertions.assertEquals(Strings.repeat("2", 3), namePairs.get("1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng")); + Assertions.assertEquals(Strings.repeat("3", 129), namePairs.get("0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng")); + Assertions.assertEquals(Strings.repeat("4", 130), namePairs.get("CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng")); + Assertions.assertEquals(Strings.repeat("5", 250), namePairs.get("1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng")); + Assertions.assertEquals(Strings.repeat("6", 251), namePairs.get("C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng")); } @Test From 69ef0430d53dd59d832246afd7a9e63fd79cc4d7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 09:00:40 +0200 Subject: [PATCH 05/62] added filename migration stub [ci skip] --- .../migration/v7/FilePathMigration.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java new file mode 100644 index 00000000..6e5f9a7b --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -0,0 +1,56 @@ +package org.cryptomator.cryptofs.migration.v7; + +import com.google.common.io.BaseEncoding; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.regex.Pattern; + +/** + * Helper class responsible of the migration of a single file + */ +class FilePathMigration { + + private static final Pattern BASE32_PATTERN = Pattern.compile("(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); + + private final Path oldPath; + private final String oldCanonicalName; + private final String oldFileTypePrefix; + + private FilePathMigration(Path oldPath, String oldCanonicalName, String oldFileTypePrefix) { + this.oldPath = oldPath; + this.oldCanonicalName = oldCanonicalName; + this.oldFileTypePrefix = oldFileTypePrefix; + } + + /** + * Starts a migration of the given file. + * + * @param vaultRoot Path to the vault's base directory (parent of d/ and m/). + * @param oldPath Path of an existing file inside the d/ directory of a vault. May be a normal file, directory file or symlink as well as conflicting copies. + * @return A new instance of FileNameMigration + * @throws IOException Non-recoverable I/O error, e.g. if a .lng file could not be inflated due to missing metadata. + */ + static FilePathMigration parse(Path vaultRoot, Path oldPath) throws IOException { + // TODO 1. extract canonical name + // TODO 2. inflate + // TODO 3. determine whether DIRECTORY or SYMLINK + // TODO 4. determine whether any conflict pre- or suffixes exist + return new FilePathMigration(oldPath, null, null); + } + + /** + * @return The path after migrating + * @throws IOException Non-recoverable I/O error + */ + Path migrate() throws IOException { + // TODO 5. reencode and add new file extension + // TODO 6. deflate if exceeding new threshold + // TODO 7. in case of DIRECTORY or SYMLINK: create parent dir? + // TODO 8. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException + return null; + } + +} From 19c6649c05b0f8cd9317424663f6fc80a919a940 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 09:43:40 +0200 Subject: [PATCH 06/62] more stubs and tests [ci skip] --- .../migration/v7/FilePathMigration.java | 70 +++++++++++++++--- .../migration/v7/FilePathMigrationTest.java | 74 +++++++++++++++++++ 2 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 6e5f9a7b..6347dd93 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -8,21 +8,27 @@ /** * Helper class responsible of the migration of a single file + *

+ * Filename migration is a two-step process: Disassembly of the old path and assembly of a new path. */ class FilePathMigration { private static final Pattern BASE32_PATTERN = Pattern.compile("(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); + static final int SHORTENING_THRESHOLD = 222; // see calculations in https://github.com/cryptomator/cryptofs/issues/60 private final Path oldPath; private final String oldCanonicalName; - private final String oldFileTypePrefix; - private FilePathMigration(Path oldPath, String oldCanonicalName, String oldFileTypePrefix) { + /** + * @param oldPath The actual file path before migration + * @param oldCanonicalName The inflated old filename without any conflicting pre- or suffixes but including the file type prefix + */ + FilePathMigration(Path oldPath, String oldCanonicalName) { + assert BASE32_PATTERN.matcher(oldCanonicalName).matches(); this.oldPath = oldPath; this.oldCanonicalName = oldCanonicalName; - this.oldFileTypePrefix = oldFileTypePrefix; } /** @@ -36,21 +42,63 @@ private FilePathMigration(Path oldPath, String oldCanonicalName, String oldFileT static FilePathMigration parse(Path vaultRoot, Path oldPath) throws IOException { // TODO 1. extract canonical name // TODO 2. inflate - // TODO 3. determine whether DIRECTORY or SYMLINK - // TODO 4. determine whether any conflict pre- or suffixes exist - return new FilePathMigration(oldPath, null, null); + // TODO 3. determine whether any conflict pre- or suffixes exist + return new FilePathMigration(oldPath, null); } /** + * Migrates the path. This method attempts to give a migrated file its canonical name. + * In case of conflicts with existing files a suffix will be added, which will later trigger the conflict resolver. + * * @return The path after migrating * @throws IOException Non-recoverable I/O error */ Path migrate() throws IOException { - // TODO 5. reencode and add new file extension - // TODO 6. deflate if exceeding new threshold - // TODO 7. in case of DIRECTORY or SYMLINK: create parent dir? - // TODO 8. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException + // TODO 4. reencode and add new file extension + // TODO 5. deflate if exceeding new threshold + // TODO 6. in case of DIRECTORY or SYMLINK: create parent dir? + // TODO 7. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException return null; } - + + /** + * @return {@link #oldCanonicalName} without any preceeding "0" or "1S" in case of dirs or symlinks. + */ + // visible for testing + String getOldCanonicalNameWithoutTypePrefix() { + return null; // TODO + } + + /** + * @return BASE64-encode(BASE32-decode({@link #getOldCanonicalNameWithoutTypePrefix oldCanonicalNameWithoutPrefix})) + ".c9r" + */ + // visible for testing + String getNewInflatedName() { + return null; // TODO + } + + /** + * @return {@link #getNewInflatedName() newInflatedName} if it is shorter than {@link #SHORTENING_THRESHOLD}, else SHA1(newInflatedName) + ".c9s" + */ + // visible for testing + String getNewDeflatedName() { + return null; // TODO + } + + /** + * @return true if {@link #oldCanonicalName} starts with "0" + */ + // visible for testing + boolean isDirectory() { + return false; // TODO + } + + /** + * @return true if {@link #oldCanonicalName} starts with "1S" + */ + // visible for testing + boolean isSymlink() { + return false; // TODO + } + } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java new file mode 100644 index 00000000..537729d7 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -0,0 +1,74 @@ +package org.cryptomator.cryptofs.migration.v7; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.nio.file.Path; + +public class FilePathMigrationTest { + + private Path vaultRoot = Mockito.mock(Path.class, "vaultRoot"); + + @ParameterizedTest(name = "getOldCanonicalNameWithoutTypePrefix() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,ORSXG5A=", + "0ORSXG5A=,ORSXG5A=", + "1SORSXG5A=,ORSXG5A=", + }) + public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, boolean expectedResult) { + FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.getOldCanonicalNameWithoutTypePrefix()); + } + + @ParameterizedTest(name = "getNewInflatedName() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,dGVzdA==.c9r", + "0ORSXG5A=,dGVzdA==.c9r", + "1SORSXG5A=,dGVzdA==.c9r", + }) + public void testGetNewInflatedName(String oldCanonicalName, boolean expectedResult) { + FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.getNewInflatedName()); + } + + @ParameterizedTest(name = "getNewInflatedName() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,dGVzdA==.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSQ====,dGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRl.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,df4c6d4b7623b22309470bb5a0055cff44b66805.c9s", + }) + public void testGetNewDeflatedName(String oldCanonicalName, boolean expectedResult) { + FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.getNewDeflatedName()); + } + + @ParameterizedTest(name = "isDirectory() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,false", + "0ORSXG5A=,true", + "1SORSXG5A=,false", + }) + public void testIsDirectory(String oldCanonicalName, boolean expectedResult) { + FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.isDirectory()); + } + + @ParameterizedTest(name = "isSymlink() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,false", + "0ORSXG5A=,false", + "1SORSXG5A=,true", + }) + public void testIsSymlink(String oldCanonicalName, boolean expectedResult) { + FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.isSymlink()); + } + +} From 41b3a04430950310934fd30b03b9098eaffdf059 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 09:54:21 +0200 Subject: [PATCH 07/62] updated dependencies [ci skip] --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index ab3e4011..ffa560a7 100644 --- a/pom.xml +++ b/pom.xml @@ -14,13 +14,13 @@ - 1.2.1 - 2.22.1 - 27.1-jre - 1.7.26 + 1.2.2 + 2.24 + 28.0-jre + 1.7.28 - 5.4.2 - 2.27.0 + 5.5.1 + 3.0.0 2.1 UTF-8 From 8fa49b1cbfed0cfb20c0e60e89f1a777cdc74815 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 10:05:18 +0200 Subject: [PATCH 08/62] implemented stubs and fixed some tests --- .../migration/v7/FilePathMigration.java | 37 +++++++++++++++---- .../migration/v7/FilePathMigrationTest.java | 8 ++-- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 6347dd93..2ac8b208 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -1,11 +1,14 @@ package org.cryptomator.cryptofs.migration.v7; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptolib.common.MessageDigestSupplier; import java.io.IOException; import java.nio.file.Path; import java.util.regex.Pattern; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * Helper class responsible of the migration of a single file *

@@ -16,7 +19,11 @@ class FilePathMigration { private static final Pattern BASE32_PATTERN = Pattern.compile("(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); - static final int SHORTENING_THRESHOLD = 222; // see calculations in https://github.com/cryptomator/cryptofs/issues/60 + private static final int SHORTENING_THRESHOLD = 222; // see calculations in https://github.com/cryptomator/cryptofs/issues/60 + private static final String OLD_DIRECTORY_PREFIX = "0"; + private static final String OLD_SYMLINK_PREFIX = "1S"; + private static final String NEW_REGULAR_SUFFIX = ".c9r"; + private static final String NEW_SHORTENED_SUFFIX = ".c9s"; private final Path oldPath; private final String oldCanonicalName; @@ -66,23 +73,37 @@ Path migrate() throws IOException { */ // visible for testing String getOldCanonicalNameWithoutTypePrefix() { - return null; // TODO + if (oldCanonicalName.startsWith(OLD_DIRECTORY_PREFIX)) { + return oldCanonicalName.substring(OLD_DIRECTORY_PREFIX.length()); + } else if (oldCanonicalName.startsWith(OLD_SYMLINK_PREFIX)) { + return oldCanonicalName.substring(OLD_SYMLINK_PREFIX.length()); + } else { + return oldCanonicalName; + } } /** - * @return BASE64-encode(BASE32-decode({@link #getOldCanonicalNameWithoutTypePrefix oldCanonicalNameWithoutPrefix})) + ".c9r" + * @return BASE64-encode(BASE32-decode({@link #getOldCanonicalNameWithoutTypePrefix oldCanonicalNameWithoutPrefix})) + {@value #NEW_REGULAR_SUFFIX} */ // visible for testing String getNewInflatedName() { - return null; // TODO + byte[] decoded = BASE32.decode(getOldCanonicalNameWithoutTypePrefix()); + return BASE64.encode(decoded) + NEW_REGULAR_SUFFIX; } /** - * @return {@link #getNewInflatedName() newInflatedName} if it is shorter than {@link #SHORTENING_THRESHOLD}, else SHA1(newInflatedName) + ".c9s" + * @return {@link #getNewInflatedName() newInflatedName} if it is shorter than {@link #SHORTENING_THRESHOLD}, else BASE64(SHA1(newInflatedName)) + ".c9s" */ // visible for testing String getNewDeflatedName() { - return null; // TODO + String inflatedName = getNewInflatedName(); + if (inflatedName.length() > SHORTENING_THRESHOLD) { + byte[] longFileNameBytes = inflatedName.getBytes(UTF_8); + byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); + return BASE64.encode(hash) + NEW_SHORTENED_SUFFIX; + } else { + return inflatedName; + } } /** @@ -90,7 +111,7 @@ String getNewDeflatedName() { */ // visible for testing boolean isDirectory() { - return false; // TODO + return oldCanonicalName.startsWith(OLD_DIRECTORY_PREFIX); } /** @@ -98,7 +119,7 @@ boolean isDirectory() { */ // visible for testing boolean isSymlink() { - return false; // TODO + return oldCanonicalName.startsWith(OLD_SYMLINK_PREFIX); } } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index 537729d7..4fd76519 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -17,7 +17,7 @@ public class FilePathMigrationTest { "0ORSXG5A=,ORSXG5A=", "1SORSXG5A=,ORSXG5A=", }) - public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, boolean expectedResult) { + public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, String expectedResult) { FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getOldCanonicalNameWithoutTypePrefix()); @@ -29,7 +29,7 @@ public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, bo "0ORSXG5A=,dGVzdA==.c9r", "1SORSXG5A=,dGVzdA==.c9r", }) - public void testGetNewInflatedName(String oldCanonicalName, boolean expectedResult) { + public void testGetNewInflatedName(String oldCanonicalName, String expectedResult) { FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getNewInflatedName()); @@ -39,9 +39,9 @@ public void testGetNewInflatedName(String oldCanonicalName, boolean expectedResu @CsvSource({ "ORSXG5A=,dGVzdA==.c9r", "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSQ====,dGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRl.c9r", - "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,df4c6d4b7623b22309470bb5a0055cff44b66805.c9s", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s", }) - public void testGetNewDeflatedName(String oldCanonicalName, boolean expectedResult) { + public void testGetNewDeflatedName(String oldCanonicalName, String expectedResult) { FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getNewDeflatedName()); From 208b9076baa8a1c326d05db2e10a30238bf6edfb Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 11:28:31 +0200 Subject: [PATCH 09/62] added further tests [ci skip] --- .../migration/v7/FilePathMigration.java | 5 ++ .../migration/v7/FilePathMigrationTest.java | 84 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 2ac8b208..8ce8adb1 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -67,6 +67,11 @@ Path migrate() throws IOException { // TODO 7. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException return null; } + + // visible for testing + String getOldCanonicalName() { + return oldCanonicalName; + } /** * @return {@link #oldCanonicalName} without any preceeding "0" or "1S" in case of dirs or symlinks. diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index 4fd76519..a8ceba49 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -1,12 +1,30 @@ package org.cryptomator.cryptofs.migration.v7; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mockito; +import sun.jvm.hotspot.utilities.Assert; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.Files; import java.nio.file.Path; +import static java.nio.charset.StandardCharsets.UTF_8; + public class FilePathMigrationTest { private Path vaultRoot = Mockito.mock(Path.class, "vaultRoot"); @@ -71,4 +89,70 @@ public void testIsSymlink(String oldCanonicalName, boolean expectedResult) { Assertions.assertEquals(expectedResult, migration.isSymlink()); } + @DisplayName("FilePathMigration.parse(...)") + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Parsing { + + private FileSystem fs; + private Path dataDir; + private Path metaDir; + + @BeforeAll + public void beforeAll() { + fs = Jimfs.newFileSystem(Configuration.unix()); + vaultRoot = fs.getPath("/vaultDir"); + dataDir = vaultRoot.resolve("d"); + metaDir = vaultRoot.resolve("m"); + } + + @BeforeEach + public void beforeEach() throws IOException { + Files.createDirectory(vaultRoot); + Files.createDirectory(dataDir); + Files.createDirectory(metaDir); + } + + @AfterEach + public void afterEach() throws IOException { + MoreFiles.deleteRecursively(vaultRoot, RecursiveDeleteOption.ALLOW_INSECURE); + } + + @DisplayName("regular files") + @ParameterizedTest(name = "parse(vaultRoot, {0}).getOldCanonicalName() expected to be {1}") + @CsvSource({ + "00/000000000000000000000000000000/ORSXG5A=,ORSXG5A=", + "00/000000000000000000000000000000/0ORSXG5A=,0ORSXG5A=", + "00/000000000000000000000000000000/1SORSXG5A=,1SORSXG5A=", + // TODO: add conflicting files + }) + public void testParseNonShortenedFile(String oldPath, String expected) throws IOException { + Path path = dataDir.resolve(oldPath); + + FilePathMigration migration = FilePathMigration.parse(vaultRoot, path); + + Assertions.assertEquals(expected, migration.getOldCanonicalName()); + } + + @DisplayName("shortened files") + @ParameterizedTest(name = "parse(vaultRoot, {0}).getOldCanonicalName() expected to be {2}") + @CsvSource({ + "00/000000000000000000000000000000/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,ORSXG5A=", + "00/000000000000000000000000000000/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,ZN/PC/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,0ORSXG5A=", + "00/000000000000000000000000000000/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=", + // TODO: add conflicting files + }) + public void testParseShortenedFile(String oldPath, String metadataFilePath, String expected) throws IOException { + Path path = dataDir.resolve(oldPath); + Path lngFilePath = metaDir.resolve(metadataFilePath); + Files.createDirectories(lngFilePath.getParent()); + Files.write(lngFilePath, expected.getBytes(UTF_8)); + + FilePathMigration migration = FilePathMigration.parse(vaultRoot, path); + + Assertions.assertEquals(expected, migration.getOldCanonicalName()); + } + + } + } From d1d1750c5ab8740af90e94a5903d5f6d6b017bf4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 12:14:34 +0200 Subject: [PATCH 10/62] parse filenames from old vault format 6 --- .../migration/v7/FilePathMigration.java | 47 +++++++++++++++---- .../migration/v7/FilePathMigrationTest.java | 31 +++++++++--- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 8ce8adb1..835e2467 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -4,7 +4,10 @@ import org.cryptomator.cryptolib.common.MessageDigestSupplier; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; +import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.nio.charset.StandardCharsets.UTF_8; @@ -16,7 +19,9 @@ */ class FilePathMigration { - private static final Pattern BASE32_PATTERN = Pattern.compile("(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); + private static final String OLD_SHORTENED_FILENAME_SUFFIX = ".lng"; + private static final Pattern OLD_SHORTENED_FILENAME_PATTERN = Pattern.compile("[A-Z2-7]{32}\\.lng"); + private static final Pattern OLD_CANONICAL_FILENAME_PATTERN = Pattern.compile("(0|1S)?([A-Z2-7]{8})*?[A-Z2-7=]{8}"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); private static final int SHORTENING_THRESHOLD = 222; // see calculations in https://github.com/cryptomator/cryptofs/issues/60 @@ -29,11 +34,11 @@ class FilePathMigration { private final String oldCanonicalName; /** - * @param oldPath The actual file path before migration + * @param oldPath The actual file path before migration * @param oldCanonicalName The inflated old filename without any conflicting pre- or suffixes but including the file type prefix */ FilePathMigration(Path oldPath, String oldCanonicalName) { - assert BASE32_PATTERN.matcher(oldCanonicalName).matches(); + assert OLD_CANONICAL_FILENAME_PATTERN.matcher(oldCanonicalName).matches(); this.oldPath = oldPath; this.oldCanonicalName = oldCanonicalName; } @@ -46,11 +51,33 @@ class FilePathMigration { * @return A new instance of FileNameMigration * @throws IOException Non-recoverable I/O error, e.g. if a .lng file could not be inflated due to missing metadata. */ - static FilePathMigration parse(Path vaultRoot, Path oldPath) throws IOException { - // TODO 1. extract canonical name - // TODO 2. inflate - // TODO 3. determine whether any conflict pre- or suffixes exist - return new FilePathMigration(oldPath, null); + public static Optional parse(Path vaultRoot, Path oldPath) throws IOException { + final String oldFileName = oldPath.getFileName().toString(); + final String canonicalOldFileName; + if (oldFileName.endsWith(OLD_SHORTENED_FILENAME_SUFFIX)) { + Matcher matcher = OLD_SHORTENED_FILENAME_PATTERN.matcher(oldFileName); + if (matcher.find()) { + canonicalOldFileName = inflate(vaultRoot, matcher.group()); + } else { + return Optional.empty(); + } + } else { + Matcher matcher = OLD_CANONICAL_FILENAME_PATTERN.matcher(oldFileName); + if (matcher.find()) { + canonicalOldFileName = matcher.group(); + } else { + return Optional.empty(); + } + } + return Optional.of(new FilePathMigration(oldPath, canonicalOldFileName)); + } + + // visible for testing + static String inflate(Path vaultRoot, String canonicalLongFileName) throws IOException { + Path metadataFilePath = vaultRoot.resolve("m/" + canonicalLongFileName.substring(0, 2) + "/" + canonicalLongFileName.substring(2, 4) + "/" + canonicalLongFileName); + byte[] contents = Files.readAllBytes(metadataFilePath); // TODO max buffer size... + // TODO... if metadatafile missing throw explicit exception? + return new String(contents, UTF_8); } /** @@ -60,14 +87,14 @@ static FilePathMigration parse(Path vaultRoot, Path oldPath) throws IOException * @return The path after migrating * @throws IOException Non-recoverable I/O error */ - Path migrate() throws IOException { + public Path migrate() throws IOException { // TODO 4. reencode and add new file extension // TODO 5. deflate if exceeding new threshold // TODO 6. in case of DIRECTORY or SYMLINK: create parent dir? // TODO 7. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException return null; } - + // visible for testing String getOldCanonicalName() { return oldCanonicalName; diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index a8ceba49..c12c71fb 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -10,18 +10,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -import sun.jvm.hotspot.utilities.Assert; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; @@ -118,6 +117,22 @@ public void afterEach() throws IOException { MoreFiles.deleteRecursively(vaultRoot, RecursiveDeleteOption.ALLOW_INSECURE); } + @DisplayName("unrelated files") + @ParameterizedTest(name = "parse(vaultRoot, {0}) expected to be unparsable") + @ValueSource(strings = { + "00/000000000000000000000000000000/.DS_Store", + "00/000000000000000000000000000000/foo", + "00/000000000000000000000000000000/ORSXG5A", // removed one char + "00/000000000000000000000000000000/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7H.lng", // removed one char + }) + public void testParseUnrelatedFile(String oldPath) throws IOException { + Path path = dataDir.resolve(oldPath); + + Optional migration = FilePathMigration.parse(vaultRoot, path); + + Assertions.assertFalse(migration.isPresent()); + } + @DisplayName("regular files") @ParameterizedTest(name = "parse(vaultRoot, {0}).getOldCanonicalName() expected to be {1}") @CsvSource({ @@ -129,9 +144,10 @@ public void afterEach() throws IOException { public void testParseNonShortenedFile(String oldPath, String expected) throws IOException { Path path = dataDir.resolve(oldPath); - FilePathMigration migration = FilePathMigration.parse(vaultRoot, path); + Optional migration = FilePathMigration.parse(vaultRoot, path); - Assertions.assertEquals(expected, migration.getOldCanonicalName()); + Assertions.assertTrue(migration.isPresent()); + Assertions.assertEquals(expected, migration.get().getOldCanonicalName()); } @DisplayName("shortened files") @@ -148,9 +164,10 @@ public void testParseShortenedFile(String oldPath, String metadataFilePath, Stri Files.createDirectories(lngFilePath.getParent()); Files.write(lngFilePath, expected.getBytes(UTF_8)); - FilePathMigration migration = FilePathMigration.parse(vaultRoot, path); + Optional migration = FilePathMigration.parse(vaultRoot, path); - Assertions.assertEquals(expected, migration.getOldCanonicalName()); + Assertions.assertTrue(migration.isPresent()); + Assertions.assertEquals(expected, migration.get().getOldCanonicalName()); } } From 5f3933a34a1150fde827fe7514a420c9422686a2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 14:24:18 +0200 Subject: [PATCH 11/62] more tests --- .../migration/v7/FilePathMigration.java | 26 ++++++++--- .../v7/UninflatableFileException.java | 14 ++++++ .../migration/v7/FilePathMigrationTest.java | 45 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v7/UninflatableFileException.java diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 835e2467..5e868b6d 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -1,11 +1,17 @@ package org.cryptomator.cryptofs.migration.v7; +import com.google.common.base.Throwables; import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,6 +35,7 @@ class FilePathMigration { private static final String OLD_SYMLINK_PREFIX = "1S"; private static final String NEW_REGULAR_SUFFIX = ".c9r"; private static final String NEW_SHORTENED_SUFFIX = ".c9s"; + private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; private final Path oldPath; private final String oldCanonicalName; @@ -49,7 +56,7 @@ class FilePathMigration { * @param vaultRoot Path to the vault's base directory (parent of d/ and m/). * @param oldPath Path of an existing file inside the d/ directory of a vault. May be a normal file, directory file or symlink as well as conflicting copies. * @return A new instance of FileNameMigration - * @throws IOException Non-recoverable I/O error, e.g. if a .lng file could not be inflated due to missing metadata. + * @throws IOException Non-recoverable I/O error, such as {@link UninflatableFileException}s */ public static Optional parse(Path vaultRoot, Path oldPath) throws IOException { final String oldFileName = oldPath.getFileName().toString(); @@ -73,11 +80,20 @@ public static Optional parse(Path vaultRoot, Path oldPath) th } // visible for testing - static String inflate(Path vaultRoot, String canonicalLongFileName) throws IOException { + static String inflate(Path vaultRoot, String canonicalLongFileName) throws UninflatableFileException { Path metadataFilePath = vaultRoot.resolve("m/" + canonicalLongFileName.substring(0, 2) + "/" + canonicalLongFileName.substring(2, 4) + "/" + canonicalLongFileName); - byte[] contents = Files.readAllBytes(metadataFilePath); // TODO max buffer size... - // TODO... if metadatafile missing throw explicit exception? - return new String(contents, UTF_8); + try (SeekableByteChannel ch = Files.newByteChannel(metadataFilePath, StandardOpenOption.READ)) { + if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { + throw new UninflatableFileException("Unexpectedly large file: " + metadataFilePath); + } + ByteBuffer buf = ByteBuffer.allocate((int) Math.min(ch.size(), MAX_FILENAME_BUFFER_SIZE)); + ch.read(buf); + buf.flip(); + return UTF_8.decode(buf).toString(); + } catch (IOException e) { + Throwables.throwIfInstanceOf(e, UninflatableFileException.class); + throw new UninflatableFileException("Failed to read metadata file " + metadataFilePath, e); + } } /** diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/UninflatableFileException.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/UninflatableFileException.java new file mode 100644 index 00000000..8d5e4705 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/UninflatableFileException.java @@ -0,0 +1,14 @@ +package org.cryptomator.cryptofs.migration.v7; + +import java.io.IOException; + +public class UninflatableFileException extends IOException { + + public UninflatableFileException(String message) { + super(message); + } + + public UninflatableFileException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index c12c71fb..70f67dff 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -4,12 +4,15 @@ import com.google.common.io.RecursiveDeleteOption; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -19,6 +22,7 @@ import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Optional; @@ -117,6 +121,47 @@ public void afterEach() throws IOException { MoreFiles.deleteRecursively(vaultRoot, RecursiveDeleteOption.ALLOW_INSECURE); } + @DisplayName("inflate with non-existing metadata file") + @Test + public void testInflateWithMissingMetadata() { + UninflatableFileException e = Assertions.assertThrows(UninflatableFileException.class, () -> { + FilePathMigration.inflate(vaultRoot, "NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng"); + + }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(NoSuchFileException.class)); + } + + @DisplayName("inflate with too large metadata file") + @Test + public void testInflateWithTooLargeMetadata() throws IOException { + Path lngFilePath = metaDir.resolve("NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng"); + Files.createDirectories(lngFilePath.getParent()); + Files.write(lngFilePath, new byte[10*1024+1]); + + UninflatableFileException e = Assertions.assertThrows(UninflatableFileException.class, () -> { + FilePathMigration.inflate(vaultRoot, "NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng"); + + }); + MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("Unexpectedly large file")); + } + + @DisplayName("inflate") + @ParameterizedTest(name = "inflate(vaultRoot, {0})") + @CsvSource({ + "NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,ORSXG5A=", + "ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,ZN/PC/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,0ORSXG5A=", + "NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=", + }) + public void testInflate(String canonicalLongFileName, String metadataFilePath, String expected) throws IOException { + Path lngFilePath = metaDir.resolve(metadataFilePath); + Files.createDirectories(lngFilePath.getParent()); + Files.write(lngFilePath, expected.getBytes(UTF_8)); + + String result = FilePathMigration.inflate(vaultRoot, canonicalLongFileName); + + Assertions.assertEquals(expected, result); + } + @DisplayName("unrelated files") @ParameterizedTest(name = "parse(vaultRoot, {0}) expected to be unparsable") @ValueSource(strings = { From dfaecc04ebd82f09be5168b29c36c76f75ecdb5e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 17:30:09 +0200 Subject: [PATCH 12/62] implemented migration of a single vault format 6 path --- .../migration/v7/FilePathMigration.java | 89 ++++++++++-- .../migration/v7/FilePathMigrationTest.java | 135 +++++++++++++++--- 2 files changed, 194 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 5e868b6d..a774537d 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -5,11 +5,10 @@ import org.cryptomator.cryptolib.common.MessageDigestSupplier; import java.io.IOException; -import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Optional; @@ -26,7 +25,7 @@ class FilePathMigration { private static final String OLD_SHORTENED_FILENAME_SUFFIX = ".lng"; - private static final Pattern OLD_SHORTENED_FILENAME_PATTERN = Pattern.compile("[A-Z2-7]{32}\\.lng"); + private static final Pattern OLD_SHORTENED_FILENAME_PATTERN = Pattern.compile("[A-Z2-7]{32}"); private static final Pattern OLD_CANONICAL_FILENAME_PATTERN = Pattern.compile("(0|1S)?([A-Z2-7]{8})*?[A-Z2-7=]{8}"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); @@ -36,12 +35,16 @@ class FilePathMigration { private static final String NEW_REGULAR_SUFFIX = ".c9r"; private static final String NEW_SHORTENED_SUFFIX = ".c9s"; private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; + private static final String NEW_SHORTENED_METADATA_FILE = "name.c9s"; + private static final String NEW_DIR_FILE = "dir.c9r"; + private static final String NEW_CONTENTS_FILE = "contents.c9r"; + private static final String NEW_SYMLINK_FILE = "symlink.c9r"; private final Path oldPath; private final String oldCanonicalName; /** - * @param oldPath The actual file path before migration + * @param oldPath The actual file path before migration * @param oldCanonicalName The inflated old filename without any conflicting pre- or suffixes but including the file type prefix */ FilePathMigration(Path oldPath, String oldCanonicalName) { @@ -54,7 +57,7 @@ class FilePathMigration { * Starts a migration of the given file. * * @param vaultRoot Path to the vault's base directory (parent of d/ and m/). - * @param oldPath Path of an existing file inside the d/ directory of a vault. May be a normal file, directory file or symlink as well as conflicting copies. + * @param oldPath Path of an existing file inside the d/ directory of a vault. May be a normal file, directory file or symlink as well as conflicting copies. * @return A new instance of FileNameMigration * @throws IOException Non-recoverable I/O error, such as {@link UninflatableFileException}s */ @@ -64,7 +67,7 @@ public static Optional parse(Path vaultRoot, Path oldPath) th if (oldFileName.endsWith(OLD_SHORTENED_FILENAME_SUFFIX)) { Matcher matcher = OLD_SHORTENED_FILENAME_PATTERN.matcher(oldFileName); if (matcher.find()) { - canonicalOldFileName = inflate(vaultRoot, matcher.group()); + canonicalOldFileName = inflate(vaultRoot, matcher.group() + OLD_SHORTENED_FILENAME_SUFFIX); } else { return Optional.empty(); } @@ -79,9 +82,17 @@ public static Optional parse(Path vaultRoot, Path oldPath) th return Optional.of(new FilePathMigration(oldPath, canonicalOldFileName)); } + /** + * Resolves the canonical name of a deflated file represented by the given longFileName. + * + * @param vaultRoot Path to the vault's base directory (parent of d/ and m/). + * @param longFileName Canonical name of the {@value #OLD_SHORTENED_FILENAME_SUFFIX} file. + * @return The inflated filename + * @throws UninflatableFileException If the file could not be inflated due to missing or malformed metadata. + */ // visible for testing - static String inflate(Path vaultRoot, String canonicalLongFileName) throws UninflatableFileException { - Path metadataFilePath = vaultRoot.resolve("m/" + canonicalLongFileName.substring(0, 2) + "/" + canonicalLongFileName.substring(2, 4) + "/" + canonicalLongFileName); + static String inflate(Path vaultRoot, String longFileName) throws UninflatableFileException { + Path metadataFilePath = vaultRoot.resolve("m/" + longFileName.substring(0, 2) + "/" + longFileName.substring(2, 4) + "/" + longFileName); try (SeekableByteChannel ch = Files.newByteChannel(metadataFilePath, StandardOpenOption.READ)) { if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { throw new UninflatableFileException("Unexpectedly large file: " + metadataFilePath); @@ -104,11 +115,63 @@ static String inflate(Path vaultRoot, String canonicalLongFileName) throws Uninf * @throws IOException Non-recoverable I/O error */ public Path migrate() throws IOException { - // TODO 4. reencode and add new file extension - // TODO 5. deflate if exceeding new threshold - // TODO 6. in case of DIRECTORY or SYMLINK: create parent dir? - // TODO 7. attempt MOVE, retry with conflict-suffix up to N times in case of FileAlreadyExistsException - return null; + final String canonicalInflatedName = getNewInflatedName(); + final String canonicalDeflatedName = getNewDeflatedName(); + final boolean isShortened = !canonicalInflatedName.equals(canonicalDeflatedName); + + FileAlreadyExistsException attemptsExceeded = new FileAlreadyExistsException(oldPath.toString(), oldPath.resolveSibling(canonicalDeflatedName).toString(), ""); + String attemptSuffix = ""; + + for (int i = 1; i <= 3; i++) { + try { + Path newPath = getTargetPath(attemptSuffix); + if (isShortened || isDirectory() || isSymlink()) { + Files.createDirectory(newPath.getParent()); + } + if (isShortened) { + Path metadataFilePath = newPath.resolveSibling(NEW_SHORTENED_METADATA_FILE); + Files.write(metadataFilePath, canonicalInflatedName.getBytes(UTF_8)); + } + return Files.move(oldPath, newPath); + } catch (FileAlreadyExistsException e) { + attemptSuffix = "_" + i; + attemptsExceeded.addSuppressed(e); + continue; + } + } + throw attemptsExceeded; + } + + /** + * @param attemptSuffix Empty string or anything starting with a non base64 delimiter + * @return The path after successful migration of {@link #oldPath} if migration is successful for the given attemptSuffix + */ + // visible for testing + Path getTargetPath(String attemptSuffix) { + final String canonicalInflatedName = getNewInflatedName(); + final String canonicalDeflatedName = getNewDeflatedName(); + final boolean isShortened = !canonicalInflatedName.equals(canonicalDeflatedName); + + final String inflatedName = canonicalInflatedName.substring(0, canonicalInflatedName.length() - NEW_REGULAR_SUFFIX.length()) + attemptSuffix + NEW_REGULAR_SUFFIX; + final String deflatedName = canonicalDeflatedName.substring(0, canonicalDeflatedName.length() - NEW_SHORTENED_SUFFIX.length()) + attemptSuffix + NEW_SHORTENED_SUFFIX; + + if (isShortened) { + if (isDirectory()) { + return oldPath.resolveSibling(deflatedName).resolve(NEW_DIR_FILE); + } else if (isSymlink()) { + return oldPath.resolveSibling(deflatedName).resolve(NEW_SYMLINK_FILE); + } else { + return oldPath.resolveSibling(deflatedName).resolve(NEW_CONTENTS_FILE); + } + } else { + if (isDirectory()) { + return oldPath.resolveSibling(inflatedName).resolve(NEW_DIR_FILE); + } else if (isSymlink()) { + return oldPath.resolveSibling(inflatedName).resolve(NEW_SYMLINK_FILE); + } else { + return oldPath.resolveSibling(inflatedName); + } + } } // visible for testing diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index 70f67dff..ab4936bb 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -24,13 +24,14 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; public class FilePathMigrationTest { - private Path vaultRoot = Mockito.mock(Path.class, "vaultRoot"); + private Path oldPath = Mockito.mock(Path.class, "oldPath"); @ParameterizedTest(name = "getOldCanonicalNameWithoutTypePrefix() expected to be {1} for {0}") @CsvSource({ @@ -39,7 +40,7 @@ public class FilePathMigrationTest { "1SORSXG5A=,ORSXG5A=", }) public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, String expectedResult) { - FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getOldCanonicalNameWithoutTypePrefix()); } @@ -51,7 +52,7 @@ public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, St "1SORSXG5A=,dGVzdA==.c9r", }) public void testGetNewInflatedName(String oldCanonicalName, String expectedResult) { - FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getNewInflatedName()); } @@ -63,11 +64,11 @@ public void testGetNewInflatedName(String oldCanonicalName, String expectedResul "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s", }) public void testGetNewDeflatedName(String oldCanonicalName, String expectedResult) { - FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.getNewDeflatedName()); } - + @ParameterizedTest(name = "isDirectory() expected to be {1} for {0}") @CsvSource({ "ORSXG5A=,false", @@ -75,8 +76,8 @@ public void testGetNewDeflatedName(String oldCanonicalName, String expectedResul "1SORSXG5A=,false", }) public void testIsDirectory(String oldCanonicalName, boolean expectedResult) { - FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); - + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); + Assertions.assertEquals(expectedResult, migration.isDirectory()); } @@ -87,20 +88,41 @@ public void testIsDirectory(String oldCanonicalName, boolean expectedResult) { "1SORSXG5A=,true", }) public void testIsSymlink(String oldCanonicalName, boolean expectedResult) { - FilePathMigration migration = new FilePathMigration(vaultRoot, oldCanonicalName); + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); Assertions.assertEquals(expectedResult, migration.isSymlink()); } - + + @ParameterizedTest(name = "getTargetPath() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,'',dGVzdA==.c9r", + "0ORSXG5A=,'',dGVzdA==.c9r/dir.c9r", + "1SORSXG5A=,'',dGVzdA==.c9r/symlink.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,'',30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/contents.c9r", + "0ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,'',30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/dir.c9r", + "1SORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,'',30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/symlink.c9r", + "ORSXG5A=,'_1',dGVzdA==_1.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,'_123',30xtS3YjsiMJRwu1oAVc_0S2aAU=_123.c9s/contents.c9r", + }) + public void testGetTargetPath(String oldCanonicalName, String attemptSuffix, String expected) { + Path old = Paths.get("/tmp/foo"); + FilePathMigration migration = new FilePathMigration(old, oldCanonicalName); + + Path result = migration.getTargetPath(attemptSuffix); + + Assertions.assertEquals(old.resolveSibling(expected), result); + } + @DisplayName("FilePathMigration.parse(...)") @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) class Parsing { private FileSystem fs; + private Path vaultRoot; private Path dataDir; private Path metaDir; - + @BeforeAll public void beforeAll() { fs = Jimfs.newFileSystem(Configuration.unix()); @@ -108,14 +130,14 @@ public void beforeAll() { dataDir = vaultRoot.resolve("d"); metaDir = vaultRoot.resolve("m"); } - + @BeforeEach public void beforeEach() throws IOException { Files.createDirectory(vaultRoot); Files.createDirectory(dataDir); Files.createDirectory(metaDir); } - + @AfterEach public void afterEach() throws IOException { MoreFiles.deleteRecursively(vaultRoot, RecursiveDeleteOption.ALLOW_INSECURE); @@ -136,7 +158,7 @@ public void testInflateWithMissingMetadata() { public void testInflateWithTooLargeMetadata() throws IOException { Path lngFilePath = metaDir.resolve("NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng"); Files.createDirectories(lngFilePath.getParent()); - Files.write(lngFilePath, new byte[10*1024+1]); + Files.write(lngFilePath, new byte[10 * 1024 + 1]); UninflatableFileException e = Assertions.assertThrows(UninflatableFileException.class, () -> { FilePathMigration.inflate(vaultRoot, "NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng"); @@ -184,7 +206,8 @@ public void testParseUnrelatedFile(String oldPath) throws IOException { "00/000000000000000000000000000000/ORSXG5A=,ORSXG5A=", "00/000000000000000000000000000000/0ORSXG5A=,0ORSXG5A=", "00/000000000000000000000000000000/1SORSXG5A=,1SORSXG5A=", - // TODO: add conflicting files + "00/000000000000000000000000000000/conflict_1SORSXG5A=,1SORSXG5A=", + "00/000000000000000000000000000000/1SORSXG5A= (conflict),1SORSXG5A=", }) public void testParseNonShortenedFile(String oldPath, String expected) throws IOException { Path path = dataDir.resolve(oldPath); @@ -201,7 +224,8 @@ public void testParseNonShortenedFile(String oldPath, String expected) throws IO "00/000000000000000000000000000000/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,ORSXG5A=", "00/000000000000000000000000000000/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,ZN/PC/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,0ORSXG5A=", "00/000000000000000000000000000000/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=", - // TODO: add conflicting files + "00/000000000000000000000000000000/conflict_NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=", + "00/000000000000000000000000000000/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5 (conflict).lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=", }) public void testParseShortenedFile(String oldPath, String metadataFilePath, String expected) throws IOException { Path path = dataDir.resolve(oldPath); @@ -214,7 +238,84 @@ public void testParseShortenedFile(String oldPath, String metadataFilePath, Stri Assertions.assertTrue(migration.isPresent()); Assertions.assertEquals(expected, migration.get().getOldCanonicalName()); } - + + } + + @DisplayName("FilePathMigration.parse(...).get().migrate(...)") + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Migrating { + + private FileSystem fs; + private Path vaultRoot; + private Path dataDir; + private Path metaDir; + + @BeforeAll + public void beforeAll() { + fs = Jimfs.newFileSystem(Configuration.unix()); + vaultRoot = fs.getPath("/vaultDir"); + dataDir = vaultRoot.resolve("d"); + metaDir = vaultRoot.resolve("m"); + } + + @BeforeEach + public void beforeEach() throws IOException { + Files.createDirectory(vaultRoot); + Files.createDirectory(dataDir); + Files.createDirectory(metaDir); + } + + @AfterEach + public void afterEach() throws IOException { + MoreFiles.deleteRecursively(vaultRoot, RecursiveDeleteOption.ALLOW_INSECURE); + } + + @DisplayName("migrate non-shortened files") + @ParameterizedTest(name = "migrating {0} to {1}") + @CsvSource({ + "00/000000000000000000000000000000/ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r", + "00/000000000000000000000000000000/0ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/dir.c9r", + "00/000000000000000000000000000000/1SORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/symlink.c9r", + }) + public void testMigrateUnshortened(String oldPathStr, String expectedResult) throws IOException { + Path oldPath = dataDir.resolve(oldPathStr); + Files.createDirectories(oldPath.getParent()); + Files.write(oldPath, "test".getBytes(UTF_8)); + + Path newPath = FilePathMigration.parse(vaultRoot, oldPath).get().migrate(); + + Assertions.assertEquals(dataDir.resolve(expectedResult), newPath); + Assertions.assertTrue(Files.exists(newPath)); + Assertions.assertFalse(Files.exists(oldPath)); + } + + @DisplayName("migrate shortened files") + @ParameterizedTest(name = "migrating {0} to {3}") + @CsvSource({ + "00/000000000000000000000000000000/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r", + "00/000000000000000000000000000000/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,ZN/PC/ZNPCXPWRWYFOGTZHVDBOOQDYPAMKKI5R.lng,0ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/dir.c9r", + "00/000000000000000000000000000000/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,NU/C3/NUC3VFSMWKLD4526JDZKSE5V2IIMSYW5.lng,1SORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/symlink.c9r", + "00/000000000000000000000000000000/LPFZEP7JSREQMANHG7PRTOLSEKJM5JP5.lng,LP/FZ/LPFZEP7JSREQMANHG7PRTOLSEKJM5JP5.lng,ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/contents.c9r", + "00/000000000000000000000000000000/7LX7VYDWDWXRPL7ZKTTCVGUPMGPRNUSG.lng,7L/X7/7LX7VYDWDWXRPL7ZKTTCVGUPMGPRNUSG.lng,0ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/dir.c9r", + "00/000000000000000000000000000000/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ.lng,MG/BB/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ.lng,1SORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/symlink.c9r", + }) + public void testMigrateShortened(String oldPathStr, String metadataFilePath, String canonicalOldName, String expectedResult) throws IOException { + Path oldPath = dataDir.resolve(oldPathStr); + Files.createDirectories(oldPath.getParent()); + Files.write(oldPath, "test".getBytes(UTF_8)); + Path lngFilePath = metaDir.resolve(metadataFilePath); + Files.createDirectories(lngFilePath.getParent()); + Files.write(lngFilePath, canonicalOldName.getBytes(UTF_8)); + + Path newPath = FilePathMigration.parse(vaultRoot, oldPath).get().migrate(); + + Assertions.assertEquals(dataDir.resolve(expectedResult), newPath); + Assertions.assertTrue(Files.exists(newPath)); + Assertions.assertFalse(Files.exists(oldPath)); + } + + } - + } From 6a90ad4870143d2c54afc9937b2c9115d577f844 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 17:31:56 +0200 Subject: [PATCH 13/62] added more edge cases to unit test --- .../cryptofs/migration/v7/FilePathMigrationTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index ab4936bb..40901245 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -277,6 +277,9 @@ public void afterEach() throws IOException { "00/000000000000000000000000000000/ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r", "00/000000000000000000000000000000/0ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/dir.c9r", "00/000000000000000000000000000000/1SORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r/symlink.c9r", + "00/000000000000000000000000000000/conflict_ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r", + "00/000000000000000000000000000000/0ORSXG5A= (conflict),00/000000000000000000000000000000/dGVzdA==.c9r/dir.c9r", + "00/000000000000000000000000000000/conflict_1SORSXG5A= (conflict),00/000000000000000000000000000000/dGVzdA==.c9r/symlink.c9r", }) public void testMigrateUnshortened(String oldPathStr, String expectedResult) throws IOException { Path oldPath = dataDir.resolve(oldPathStr); @@ -299,6 +302,8 @@ public void testMigrateUnshortened(String oldPathStr, String expectedResult) thr "00/000000000000000000000000000000/LPFZEP7JSREQMANHG7PRTOLSEKJM5JP5.lng,LP/FZ/LPFZEP7JSREQMANHG7PRTOLSEKJM5JP5.lng,ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/contents.c9r", "00/000000000000000000000000000000/7LX7VYDWDWXRPL7ZKTTCVGUPMGPRNUSG.lng,7L/X7/7LX7VYDWDWXRPL7ZKTTCVGUPMGPRNUSG.lng,0ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/dir.c9r", "00/000000000000000000000000000000/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ.lng,MG/BB/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ.lng,1SORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/symlink.c9r", + "00/000000000000000000000000000000/conflict_NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,NT/JD/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7HF.lng,ORSXG5A=,00/000000000000000000000000000000/dGVzdA==.c9r", + "00/000000000000000000000000000000/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ (conflict).lng,MG/BB/MGBBDEW456AMIDODOA3FUOQ3WNYNQNHZ.lng,1SORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,00/000000000000000000000000000000/30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s/symlink.c9r", }) public void testMigrateShortened(String oldPathStr, String metadataFilePath, String canonicalOldName, String expectedResult) throws IOException { Path oldPath = dataDir.resolve(oldPathStr); From 93273e8f47476a0eed08df5c07a35f197a9c8342 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 18:01:44 +0200 Subject: [PATCH 14/62] removed unused code --- .../migration/v7/Version7Migrator.java | 95 ++++--------------- .../migration/v7/Version7MigratorTest.java | 47 +-------- 2 files changed, 20 insertions(+), 122 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index cd3bb1ee..e0453100 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -5,7 +5,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration.v7; -import com.google.common.io.BaseEncoding; import org.cryptomator.cryptofs.BackupUtil; import org.cryptomator.cryptofs.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; @@ -14,14 +13,12 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -31,20 +28,12 @@ import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; - -import static java.nio.charset.StandardCharsets.UTF_8; +import java.util.Optional; public class Version7Migrator implements Migrator { private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class); - private static final BaseEncoding BASE32 = BaseEncoding.base32(); - private static final int FILENAME_BUFFER_SIZE = 10 * 1024; - private static final String NEW_SHORTENED_SUFFIX = ".c9s"; - private static final String NEW_NORMAL_SUFFIX = ".c9r"; - private final CryptorProvider cryptorProvider; @Inject @@ -64,8 +53,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); - Map namePairs = loadShortenedNames(vaultRoot); - migrateFileNames(namePairs, vaultRoot); + migrateFileNames(vaultRoot); // TODO remove deprecated .lng from /m/ @@ -77,76 +65,31 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot); } - /** - * With vault format 7 we increased the file shortening threshold. - * - * @param vaultRoot - * @return - * @throws IOException - */ - // visible for testing - Map loadShortenedNames(Path vaultRoot) throws IOException { - Path metadataDir = vaultRoot.resolve("m"); - Map result = new HashMap<>(); - ByteBuffer longNameBuffer = ByteBuffer.allocate(FILENAME_BUFFER_SIZE); - Files.walkFileTree(metadataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - try (SeekableByteChannel ch = Files.newByteChannel(file, StandardOpenOption.READ)) { - if (ch.size() > longNameBuffer.capacity()) { - LOG.error("Migrator not suited to handle filenames as large as {}. Aborting without changes.", ch.size()); - throw new IOException("Filename too large for migration: " + file); - } else { - longNameBuffer.clear(); - ch.read(longNameBuffer); - longNameBuffer.flip(); - String longName = UTF_8.decode(longNameBuffer).toString(); - String shortName = file.getFileName().toString(); - result.put(shortName, longName); - return FileVisitResult.CONTINUE; - } - } - } - }); - return result; - } - - void migrateFileNames(Map deflatedNames, Path vaultRoot) throws IOException { + private void migrateFileNames(Path vaultRoot) throws IOException { Path dataDir = vaultRoot.resolve("d"); Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { @Override - public FileVisitResult visitFile(Path oldfile, BasicFileAttributes attrs) throws IOException { - String oldfilename = oldfile.getFileName().toString(); - String newfilename; - if (deflatedNames.containsKey(oldfilename)) { - newfilename = deflatedNames.get(oldfilename) + NEW_NORMAL_SUFFIX; - } else { - newfilename = oldfilename + NEW_NORMAL_SUFFIX; + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + final Optional migration; + try { + migration = FilePathMigration.parse(vaultRoot, file); + } catch (UninflatableFileException e) { + LOG.warn("SKIP {} because inflation failed.", file); + return FileVisitResult.CONTINUE; } - - if (newfilename.length() > 254) { // Value of Constants#SHORT_NAMES_MAX_LENGTH as in Vault Format 7 - newfilename = deflate(vaultRoot, newfilename); + if (migration.isPresent()) { + try { + Path migratedFile = migration.get().migrate(); + LOG.info("MOVED {} to {}", file, migratedFile); + } catch (FileAlreadyExistsException e) { + LOG.error("Failed to migrate " + file + " due to FileAlreadyExistsException. Already migrated?.", e); + return FileVisitResult.TERMINATE; + } } - - Path newfile = oldfile.resolveSibling(newfilename); - LOG.info("RENAME {} TO {}", oldfile, newfile); return FileVisitResult.CONTINUE; } }); } - String deflate(Path vaultRoot, String longName) throws IOException { - Path metadataDir = vaultRoot.resolve("m"); - byte[] longFileNameBytes = longName.getBytes(UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); - String shortName = BASE32.encode(hash) + NEW_SHORTENED_SUFFIX; - Path metadataFile = metadataDir.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName); - LOG.info("CREATE {}", metadataFile); - Files.createDirectories(metadataFile.getParent()); - Files.write(metadataFile, shortName.getBytes(UTF_8)); - return shortName; - } - } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index ceb0c2af..9f020cff 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -1,6 +1,5 @@ package org.cryptomator.cryptofs.migration.v7; -import com.google.common.base.Strings; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.migration.api.Migrator; @@ -15,11 +14,9 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; public class Version7MigratorTest { @@ -45,34 +42,6 @@ public void setup() throws IOException { KeyFile keyFile = cryptor.writeKeysToMasterkeyFile("test", 6); Files.write(masterkeyFile, keyFile.serialize()); } - Path dataFile1 = dataDir.resolve("00/000000000000000000000000000000/111"); - Path dataFile2 = dataDir.resolve("00/000000000000000000000000000000/1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng"); // 222 - Path metaFile2 = metaDir.resolve("1C/66/1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng"); - Path dataFile3 = dataDir.resolve("00/000000000000000000000000000000/0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng"); // 129 chars 33333... - Path metaFile3 = metaDir.resolve("0B/51/0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng"); - Path dataFile4 = dataDir.resolve("00/000000000000000000000000000000/CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng"); // 130 chars 44444... - Path metaFile4 = metaDir.resolve("CA/F8/CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng"); - Path dataFile5 = dataDir.resolve("00/000000000000000000000000000000/1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng"); // 250 chars 55555... - Path metaFile5 = metaDir.resolve("1C/B1/1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng"); - Path dataFile6 = dataDir.resolve("00/000000000000000000000000000000/C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng"); // 251 chars 66666... - Path metaFile6 = metaDir.resolve("C7/D6/C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng"); - Files.createDirectories(dataDir.resolve("00/000000000000000000000000000000")); - Files.createFile(dataFile1); - Files.createFile(dataFile2); - Files.createFile(dataFile3); - Files.createFile(dataFile4); - Files.createFile(dataFile5); - Files.createFile(dataFile6); - Files.createDirectories(metaFile2.getParent()); - Files.createDirectories(metaFile3.getParent()); - Files.createDirectories(metaFile4.getParent()); - Files.createDirectories(metaFile5.getParent()); - Files.createDirectories(metaFile6.getParent()); - Files.write(metaFile2, Strings.repeat("2", 3).getBytes(StandardCharsets.UTF_8)); - Files.write(metaFile3, Strings.repeat("3", 129).getBytes(StandardCharsets.UTF_8)); - Files.write(metaFile4, Strings.repeat("4", 130).getBytes(StandardCharsets.UTF_8)); - Files.write(metaFile5, Strings.repeat("5", 250).getBytes(StandardCharsets.UTF_8)); - Files.write(metaFile6, Strings.repeat("6", 251).getBytes(StandardCharsets.UTF_8)); } @AfterEach @@ -81,21 +50,7 @@ public void teardown() throws IOException { } @Test - public void testLoadShortenedNames() throws IOException { - Version7Migrator migrator = new Version7Migrator(cryptorProvider); - - Map namePairs = migrator.loadShortenedNames(vaultRoot); - - // <= 254 chars should be unshortened: - Assertions.assertEquals(Strings.repeat("2", 3), namePairs.get("1C6637A8F2E1F75E06FF9984894D6BD16A3A36A9.lng")); - Assertions.assertEquals(Strings.repeat("3", 129), namePairs.get("0B51FC45F30A0C3027F2B4C4698C5EFCA3C62FE0.lng")); - Assertions.assertEquals(Strings.repeat("4", 130), namePairs.get("CAF8F7708CBF2FD3E735A0C765EBB8E0B879360A.lng")); - Assertions.assertEquals(Strings.repeat("5", 250), namePairs.get("1CB1308D10CF786A827C91EEC1FF7B08D91ACAD6.lng")); - Assertions.assertEquals(Strings.repeat("6", 251), namePairs.get("C7D6C6201B5344583A9ED2D8F5C3239CCF666230.lng")); - } - - @Test - public void testMigration() throws IOException { + public void testKeyfileGetsUpdates() throws IOException { KeyFile beforeMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile)); Assertions.assertEquals(6, beforeMigration.getVersion()); From 238575f8dff13d2c361e2cc9a05da6d059bfa600 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 20:22:21 +0200 Subject: [PATCH 15/62] Removed permanent inflation as side effect from directory listing --- .../cryptofs/CryptoDirectoryStream.java | 13 +----------- .../CryptoDirectoryStreamIntegrationTest.java | 21 ------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java index 07725c3e..acd4c74f 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java @@ -100,12 +100,7 @@ ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { if (longFileNameProvider.isDeflated(fileName)) { try { String longFileName = longFileNameProvider.inflate(fileName); - if (longFileName.length() <= SHORT_NAMES_MAX_LENGTH) { - // "unshortify" filenames on the fly due to previously shorter threshold - return inflatePermanently(paths, longFileName); - } else { - return paths.withInflatedPath(paths.getCiphertextPath().resolveSibling(longFileName)); - } + return paths.withInflatedPath(paths.getCiphertextPath().resolveSibling(longFileName)); } catch (IOException e) { LOG.warn(paths.getCiphertextPath() + " could not be inflated."); return null; @@ -141,12 +136,6 @@ private boolean passesPlausibilityChecks(ProcessedPaths paths) { return !isBrokenDirectoryFile(paths); } - private ProcessedPaths inflatePermanently(ProcessedPaths paths, String longFileName) throws IOException { - Path newCiphertextPath = paths.getCiphertextPath().resolveSibling(longFileName); - Files.move(paths.getCiphertextPath(), newCiphertextPath); - return paths.withCiphertextPath(newCiphertextPath).withInflatedPath(newCiphertextPath); - } - private boolean isBrokenDirectoryFile(ProcessedPaths paths) { Path potentialDirectoryFile = paths.getCiphertextPath(); if (paths.getInflatedPath().getFileName().toString().startsWith(CiphertextFileType.DIRECTORY.getPrefix())) { diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java index d45adcc7..b332392d 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java @@ -83,25 +83,4 @@ public void testInflateIfNeededWithRegularLongFilename() throws IOException { MatcherAssert.assertThat(Files.exists(inflatedPath), is(false)); } - @Test - public void testInflateIfNeededWithLongFilenameThatShouldActuallyBeShort() throws IOException { - String filename = "abc"; - String inflatedName = IntStream.range(0, SHORT_NAMES_MAX_LENGTH).mapToObj(ignored -> "a").collect(Collectors.joining()); - Path ciphertextPath = fileSystem.getPath(filename); - Files.createFile(ciphertextPath); - Path inflatedPath = fileSystem.getPath(inflatedName); - when(longFileNameProvider.isDeflated(filename)).thenReturn(true); - when(longFileNameProvider.inflate(filename)).thenReturn(inflatedName); - - ProcessedPaths paths = new ProcessedPaths(ciphertextPath); - - ProcessedPaths result = inTest.inflateIfNeeded(paths); - - MatcherAssert.assertThat(result.getCiphertextPath(), is(inflatedPath)); - MatcherAssert.assertThat(result.getInflatedPath(), is(inflatedPath)); - MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); - MatcherAssert.assertThat(Files.exists(ciphertextPath), is(false)); - MatcherAssert.assertThat(Files.exists(inflatedPath), is(true)); - } - } From 604caa0582cce6a8f3313939efa5a79e25e7339a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 20:48:21 +0200 Subject: [PATCH 16/62] Removed more dead code --- .../cryptofs/EncryptedNamePattern.java | 11 --------- .../cryptofs/EncryptedNamePatternTest.java | 24 ------------------- 2 files changed, 35 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java index 5b4c7967..46280b88 100644 --- a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java +++ b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java @@ -10,23 +10,12 @@ @Singleton class EncryptedNamePattern { - private static final Pattern BASE32_PATTERN = Pattern.compile("(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); private static final Pattern BASE32_PATTERN_AT_START_OF_NAME = Pattern.compile("^(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); @Inject public EncryptedNamePattern() { } - public Optional extractEncryptedName(Path ciphertextFile) { - String name = ciphertextFile.getFileName().toString(); - Matcher matcher = BASE32_PATTERN.matcher(name); - if (matcher.find(0)) { - return Optional.of(matcher.group(2)); - } else { - return Optional.empty(); - } - } - public Optional extractEncryptedNameFromStart(Path ciphertextFile) { String name = ciphertextFile.getFileName().toString(); Matcher matcher = BASE32_PATTERN_AT_START_OF_NAME.matcher(name); diff --git a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java index f744bedb..74b3918d 100644 --- a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java +++ b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java @@ -12,25 +12,9 @@ public class EncryptedNamePatternTest { private static final String ENCRYPTED_NAME = "ALKDUEEH2445375AUZEJFEFA"; private static final Path PATH_WITHOUT_ENCRYPTED_NAME = Paths.get("foo.txt"); private static final Path PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX = Paths.get("foo" + ENCRYPTED_NAME + ".txt"); - private static final Path PATH_WITH_ENCRYPTED_NAME_AND_SUFFIX = Paths.get(ENCRYPTED_NAME + ".txt"); private EncryptedNamePattern inTest = new EncryptedNamePattern(); - @Test - public void testExtractEncryptedNameReturnsEmptyOptionalIfNoEncryptedNameIsPresent() { - Optional result = inTest.extractEncryptedName(PATH_WITHOUT_ENCRYPTED_NAME); - - Assertions.assertFalse(result.isPresent()); - } - - @Test - public void testExtractEncryptedNameReturnsEncryptedNameIfItIsIsPresent() { - Optional result = inTest.extractEncryptedName(PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX); - - Assertions.assertTrue(result.isPresent()); - Assertions.assertEquals(ENCRYPTED_NAME, result.get()); - } - @Test public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfNoEncryptedNameIsPresent() { Optional result = inTest.extractEncryptedNameFromStart(PATH_WITHOUT_ENCRYPTED_NAME); @@ -38,14 +22,6 @@ public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfNoEncryptedNa Assertions.assertFalse(result.isPresent()); } - @Test - public void testExtractEncryptedNameFromStartReturnsEncryptedNameIfItIsPresent() { - Optional result = inTest.extractEncryptedName(PATH_WITH_ENCRYPTED_NAME_AND_SUFFIX); - - Assertions.assertTrue(result.isPresent()); - Assertions.assertEquals(ENCRYPTED_NAME, result.get()); - } - @Test public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfEncryptedNameIsPresentAfterStart() { Optional result = inTest.extractEncryptedNameFromStart(PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX); From 97f473b707749daed0ac15893259d194837ab2b7 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 21 Aug 2019 23:41:41 +0200 Subject: [PATCH 17/62] Began preparing CryptoFS for Vault Format 7 [ci skip], obviously... --- .../cryptofs/CiphertextFileType.java | 1 + .../cryptofs/ConflictResolver.java | 119 ++++++---- .../org/cryptomator/cryptofs/Constants.java | 7 +- .../cryptofs/CryptoDirectoryStream.java | 2 +- .../cryptofs/CryptoPathMapper.java | 17 +- .../cryptofs/EncryptedNamePattern.java | 2 +- .../cryptofs/LongFileNameProvider.java | 77 ++++--- .../cryptofs/ConflictResolverTest.java | 217 +++++++----------- .../CryptoDirectoryStreamIntegrationTest.java | 2 +- .../cryptofs/CryptoDirectoryStreamTest.java | 2 +- .../cryptofs/LongFileNameProviderTest.java | 40 ++-- 11 files changed, 234 insertions(+), 252 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java index 1c50bc42..02cd3e2f 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java @@ -16,6 +16,7 @@ public enum CiphertextFileType { this.prefix = prefix; } + @Deprecated public String getPrefix() { return prefix; } diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java index 3bc97e2f..6273a0bf 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java @@ -1,6 +1,8 @@ package org.cryptomator.cryptofs; -import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; @@ -12,20 +14,25 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.cryptomator.cryptofs.Constants.CRYPTOMATOR_FILE_SUFFIX; +import static org.cryptomator.cryptofs.Constants.DIR_FILE_NAME; +import static org.cryptomator.cryptofs.Constants.MAX_SYMLINK_LENGTH; import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; -import static org.cryptomator.cryptofs.LongFileNameProvider.LONG_NAME_FILE_EXT; +import static org.cryptomator.cryptofs.Constants.SYMLINK_FILE_NAME; +import static org.cryptomator.cryptofs.LongFileNameProvider.SHORTENED_NAME_EXT; @CryptoFileSystemScoped class ConflictResolver { private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class); - private static final Pattern CIPHERTEXT_FILENAME_PATTERN = Pattern.compile("(0|1[A-Z0-9])?([A-Z2-7]{8})*[A-Z2-7=]{8}"); + private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}"); private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes private final LongFileNameProvider longFileNameProvider; @@ -49,11 +56,25 @@ public ConflictResolver(LongFileNameProvider longFileNameProvider, Cryptor crypt */ public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throws IOException { String ciphertextFileName = ciphertextPath.getFileName().toString(); - String basename = StringUtils.removeEnd(ciphertextFileName, LONG_NAME_FILE_EXT); - Matcher m = CIPHERTEXT_FILENAME_PATTERN.matcher(basename); + + final String basename; + final String extension; + if (ciphertextFileName.endsWith(SHORTENED_NAME_EXT)) { + basename = StringUtils.removeEnd(ciphertextFileName, SHORTENED_NAME_EXT); + extension = SHORTENED_NAME_EXT; + } else if (ciphertextFileName.endsWith(CRYPTOMATOR_FILE_SUFFIX)) { + basename = StringUtils.removeEnd(ciphertextFileName, CRYPTOMATOR_FILE_SUFFIX); + extension = CRYPTOMATOR_FILE_SUFFIX; + } else { + // file doesn't belong to the vault structure -> nothing to resolve + return ciphertextPath; + } + + Matcher m = BASE64_PATTERN.matcher(basename); if (!m.matches() && m.find(0)) { - // no full match, but still contains base32 -> partial match - return resolveConflict(ciphertextPath, m.group(0), dirId); + // no full match, but still contains base64 -> partial match + Path canonicalPath = ciphertextPath.resolveSibling(m.group() + extension); + return resolveConflict(ciphertextPath, canonicalPath, dirId); } else { // full match or no match at all -> nothing to resolve return ciphertextPath; @@ -62,38 +83,29 @@ public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throw /** * Resolves a conflict. - * - * @param conflictingPath The path of a file containing a valid base 32 part. - * @param ciphertextFileName The base32 part inside the filename of the conflicting file. + * + * @param conflictingPath The path to the potentially conflicting file. + * @param canonicalPath The path to the original (conflict-free) file. * @param dirId The directory id of the file's parent directory. * @return The new path of the conflicting file after the conflict has been resolved. * @throws IOException */ - private Path resolveConflict(Path conflictingPath, String ciphertextFileName, String dirId) throws IOException { - String conflictingFileName = conflictingPath.getFileName().toString(); - Preconditions.checkArgument(conflictingFileName.contains(ciphertextFileName), "%s does not contain %s", conflictingPath, ciphertextFileName); - - Path parent = conflictingPath.getParent(); - String inflatedFileName; - Path canonicalPath; - if (longFileNameProvider.isDeflated(conflictingFileName)) { - String deflatedName = ciphertextFileName + LONG_NAME_FILE_EXT; - inflatedFileName = longFileNameProvider.inflate(deflatedName); - canonicalPath = parent.resolve(deflatedName); - } else { - inflatedFileName = ciphertextFileName; - canonicalPath = parent.resolve(ciphertextFileName); - } - - CiphertextFileType type = CiphertextFileType.forFileName(inflatedFileName); - assert inflatedFileName.startsWith(type.getPrefix()); - String ciphertext = inflatedFileName.substring(type.getPrefix().length()); - - if (CiphertextFileType.DIRECTORY.equals(type) && resolveDirectoryConflictTrivially(canonicalPath, conflictingPath)) { + private Path resolveConflict(Path conflictingPath, Path canonicalPath, String dirId) throws IOException { + if (resolveConflictTrivially(canonicalPath, conflictingPath)) { return canonicalPath; + } + + // get ciphertext part from file: + String canonicalFileName = canonicalPath.getFileName().toString(); + String ciphertext; + if (longFileNameProvider.isDeflated(canonicalFileName)) { + String inflatedFileName = longFileNameProvider.inflate(canonicalPath); + ciphertext = StringUtils.removeEnd(inflatedFileName, CRYPTOMATOR_FILE_SUFFIX); } else { - return renameConflictingFile(canonicalPath, conflictingPath, ciphertext, dirId, type.getPrefix()); + ciphertext = StringUtils.removeEnd(canonicalFileName, CRYPTOMATOR_FILE_SUFFIX); } + + return renameConflictingFile(canonicalPath, conflictingPath, ciphertext, dirId); } /** @@ -103,22 +115,22 @@ private Path resolveConflict(Path conflictingPath, String ciphertextFileName, St * @param conflictingPath The path to the potentially conflicting file. * @param ciphertext The (previously inflated) ciphertext name of the file without any preceeding directory prefix. * @param dirId The directory id of the file's parent directory. - * @param typePrefix The prefix (if the conflicting file is a directory file or a symlink) or an empty string. * @return The new path after renaming the conflicting file. * @throws IOException */ - private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId, String typePrefix) throws IOException { + private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId) throws IOException { + assert Files.exists(canonicalPath); try { String cleartext = cryptor.fileNameCryptor().decryptFilename(ciphertext, dirId.getBytes(StandardCharsets.UTF_8)); Path alternativePath = canonicalPath; for (int i = 1; Files.exists(alternativePath); i++) { String alternativeCleartext = cleartext + " (Conflict " + i + ")"; - String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(alternativeCleartext, dirId.getBytes(StandardCharsets.UTF_8)); - String alternativeCiphertextFileName = typePrefix + alternativeCiphertext; + String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId.getBytes(StandardCharsets.UTF_8)); + String alternativeCiphertextFileName = alternativeCiphertext + CRYPTOMATOR_FILE_SUFFIX; + alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName); if (alternativeCiphertextFileName.length() > SHORT_NAMES_MAX_LENGTH) { - alternativeCiphertextFileName = longFileNameProvider.deflate(alternativeCiphertextFileName); + alternativePath = longFileNameProvider.deflate(alternativePath); } - alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName); } LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); @@ -132,21 +144,24 @@ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, Str } /** - * Tries to resolve a conflicting directory file without renaming the file. If successful, only the file with the canonical path will exist afterwards. + * Tries to resolve a conflicting file without renaming the file. If successful, only the file with the canonical path will exist afterwards. * - * @param canonicalPath The path to the original (conflict-free) directory file (must not exist). + * @param canonicalPath The path to the original (conflict-free) resource (must not exist). * @param conflictingPath The path to the potentially conflicting file (known to exist). * @return true if the conflict has been resolved. * @throws IOException */ - private boolean resolveDirectoryConflictTrivially(Path canonicalPath, Path conflictingPath) throws IOException { + private boolean resolveConflictTrivially(Path canonicalPath, Path conflictingPath) throws IOException { if (!Files.exists(canonicalPath)) { - Files.move(conflictingPath, canonicalPath, StandardCopyOption.ATOMIC_MOVE); + Files.move(conflictingPath, canonicalPath); // boom. conflict solved. return true; - } else if (hasSameDirFileContent(conflictingPath, canonicalPath)) { - // there must not be two directories pointing to the same dirId. - LOG.info("Removing conflicting directory file {} (identical to {})", conflictingPath, canonicalPath); - Files.deleteIfExists(conflictingPath); + } else if (hasSameFileContent(conflictingPath.resolve(DIR_FILE_NAME), canonicalPath.resolve(DIR_FILE_NAME), MAX_DIR_FILE_SIZE)) { + LOG.info("Removing conflicting directory {} (identical to {})", conflictingPath, canonicalPath); + MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); + return true; + } else if (hasSameFileContent(conflictingPath.resolve(SYMLINK_FILE_NAME), canonicalPath.resolve(SYMLINK_FILE_NAME), MAX_SYMLINK_LENGTH)) { + LOG.info("Removing conflicting symlink {} (identical to {})", conflictingPath, canonicalPath); + MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); return true; } else { return false; @@ -156,19 +171,25 @@ private boolean resolveDirectoryConflictTrivially(Path canonicalPath, Path confl /** * @param conflictingPath Path to a potentially conflicting file supposedly containing a directory id * @param canonicalPath Path to the canonical file containing a directory id + * @param numBytesToCompare Number of bytes to read from each file and compare to each other. * @return true if the first {@value #MAX_DIR_FILE_SIZE} bytes are equal in both files. * @throws IOException If an I/O exception occurs while reading either file. */ - private boolean hasSameDirFileContent(Path conflictingPath, Path canonicalPath) throws IOException { + private boolean hasSameFileContent(Path conflictingPath, Path canonicalPath, int numBytesToCompare) throws IOException { + if (!Files.isDirectory(conflictingPath.getParent()) || !Files.isDirectory(canonicalPath.getParent())) { + return false; + } try (ReadableByteChannel in1 = Files.newByteChannel(conflictingPath, StandardOpenOption.READ); // ReadableByteChannel in2 = Files.newByteChannel(canonicalPath, StandardOpenOption.READ)) { - ByteBuffer buf1 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE); - ByteBuffer buf2 = ByteBuffer.allocate(MAX_DIR_FILE_SIZE); + ByteBuffer buf1 = ByteBuffer.allocate(numBytesToCompare); + ByteBuffer buf2 = ByteBuffer.allocate(numBytesToCompare); int read1 = in1.read(buf1); int read2 = in2.read(buf2); buf1.flip(); buf2.flip(); return read1 == read2 && buf1.compareTo(buf2) == 0; + } catch (NoSuchFileException e) { + return false; } } diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index dbd36383..8276accd 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -14,9 +14,12 @@ public final class Constants { public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; static final String DATA_DIR_NAME = "d"; - static final String METADATA_DIR_NAME = "m"; - static final int SHORT_NAMES_MAX_LENGTH = 254; // length of a ciphertext filename before it gets shortened. It should be less than 260 (Windows MAX_PATH limit) and a multiple of 8 when the file extensions (4) and prefix (2) is subtracted. + @Deprecated static final String METADATA_DIR_NAME = "m"; + static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 static final String ROOT_DIR_ID = ""; + static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; + static final String DIR_FILE_NAME = "dir.c9r"; + static final String SYMLINK_FILE_NAME = "symlink.c9r"; static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java index acd4c74f..088bc08b 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java @@ -99,7 +99,7 @@ ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { String fileName = paths.getCiphertextPath().getFileName().toString(); if (longFileNameProvider.isDeflated(fileName)) { try { - String longFileName = longFileNameProvider.inflate(fileName); + String longFileName = longFileNameProvider.inflate(paths.getCiphertextPath()); return paths.withInflatedPath(paths.getCiphertextPath().resolveSibling(longFileName)); } catch (IOException e) { LOG.warn(paths.getCiphertextPath() + " could not be inflated."); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 832b8524..499fde9d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -78,6 +78,7 @@ public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExists * @throws NoSuchFileException If no node exists at the given path for any known type * @throws IOException */ + @Deprecated public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws NoSuchFileException, IOException { CryptoPath parentPath = cleartextPath.getParent(); if (parentPath == null) { @@ -87,7 +88,7 @@ public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws String cleartextName = cleartextPath.getFileName().toString(); NoSuchFileException notFound = new NoSuchFileException(cleartextPath.toString()); for (CiphertextFileType type : CiphertextFileType.values()) { - String ciphertextName = getCiphertextFileName(parent.dirId, cleartextName, type); + String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parent.dirId, cleartextName)); Path ciphertextPath = parent.path.resolve(ciphertextName); try { // readattr is the fastest way of checking if a file exists. Doing so in this loop is still @@ -109,21 +110,17 @@ public Path getCiphertextFilePath(CryptoPath cleartextPath, CiphertextFileType t } CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); - String ciphertextName = getCiphertextFileName(parent.dirId, cleartextName, type); - return parent.path.resolve(ciphertextName); - } - - private String getCiphertextFileName(String dirId, String cleartextName, CiphertextFileType fileType) throws IOException { - String ciphertextName = fileType.getPrefix() + ciphertextNames.getUnchecked(new DirIdAndName(dirId, cleartextName)); + String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parent.dirId, cleartextName)); + Path canonicalCiphertextPath = parent.path.resolve(ciphertextName); if (ciphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH) { - return longFileNameProvider.deflate(ciphertextName); + return longFileNameProvider.deflate(canonicalCiphertextPath); } else { - return ciphertextName; + return canonicalCiphertextPath; } } private String getCiphertextFileName(DirIdAndName dirIdAndName) { - return cryptor.fileNameCryptor().encryptFilename(dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)); + return cryptor.fileNameCryptor().encryptFilename(dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; } public void invalidatePathMapping(CryptoPath cleartextPath) { diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java index 46280b88..554705c8 100644 --- a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java +++ b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java @@ -9,7 +9,7 @@ @Singleton class EncryptedNamePattern { - + private static final Pattern BASE32_PATTERN_AT_START_OF_NAME = Pattern.compile("^(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); @Inject diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index a206b629..68770416 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -13,11 +13,14 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.migration.v7.UninflatableFileException; 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.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; @@ -29,71 +32,76 @@ import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.cryptomator.cryptofs.Constants.METADATA_DIR_NAME; @CryptoFileSystemScoped class LongFileNameProvider { - private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name. + private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); private static final Duration MAX_CACHE_AGE = Duration.ofMinutes(1); - public static final String LONG_NAME_FILE_EXT = ".lng"; + public static final String SHORTENED_NAME_EXT = ".c9s"; + private static final String LONG_NAME_FILE = "name.c9s"; - private final Path metadataRoot; private final ReadonlyFlag readonlyFlag; - private final LoadingCache longNames; + private final LoadingCache longNames; @Inject - public LongFileNameProvider(@PathToVault Path pathToVault, ReadonlyFlag readonlyFlag) { - this.metadataRoot = pathToVault.resolve(METADATA_DIR_NAME); + public LongFileNameProvider(ReadonlyFlag readonlyFlag) { this.readonlyFlag = readonlyFlag; this.longNames = CacheBuilder.newBuilder().expireAfterAccess(MAX_CACHE_AGE).build(new Loader()); } - private class Loader extends CacheLoader { + private class Loader extends CacheLoader { @Override - public String load(String shortName) throws IOException { - Path file = resolveMetadataFile(shortName); - return new String(Files.readAllBytes(file), UTF_8); + public String load(Path c9sPath) throws IOException { + Path longNameFile = c9sPath.resolve(LONG_NAME_FILE); + try (SeekableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.READ)) { + if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { + throw new UninflatableFileException("Unexpectedly large file: " + longNameFile); + } + assert ch.size() <= MAX_FILENAME_BUFFER_SIZE; + ByteBuffer buf = ByteBuffer.allocate((int) ch.size()); + ch.read(buf); + buf.flip(); + return UTF_8.decode(buf).toString(); + } } } public boolean isDeflated(String possiblyDeflatedFileName) { - return possiblyDeflatedFileName.endsWith(LONG_NAME_FILE_EXT); + return possiblyDeflatedFileName.endsWith(SHORTENED_NAME_EXT); } - public String inflate(String shortFileName) throws IOException { + public String inflate(Path c9sPath) throws IOException { try { - return longNames.get(shortFileName); + return longNames.get(c9sPath); } catch (ExecutionException e) { Throwables.throwIfInstanceOf(e.getCause(), IOException.class); throw new IllegalStateException("Unexpected exception", e); } } - public String deflate(String longFileName) { + public Path deflate(Path canonicalFileName) { + String longFileName = canonicalFileName.getFileName().toString(); byte[] longFileNameBytes = longFileName.getBytes(UTF_8); byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); - String shortName = BASE32.encode(hash) + LONG_NAME_FILE_EXT; + String shortName = BASE64.encode(hash) + SHORTENED_NAME_EXT; + Path result = canonicalFileName.resolveSibling(shortName); String cachedLongName = longNames.getIfPresent(shortName); if (cachedLongName == null) { - longNames.put(shortName, longFileName); + longNames.put(result, longFileName); } else { assert cachedLongName.equals(longFileName); } - return shortName; - } - - private Path resolveMetadataFile(String shortName) { - return metadataRoot.resolve(shortName.substring(0, 2)).resolve(shortName.substring(2, 4)).resolve(shortName); + return result; } - public Optional getCached(Path ciphertextFile) { - String shortName = ciphertextFile.getFileName().toString(); - String longName = longNames.getIfPresent(shortName); + public Optional getCached(Path c9sPath) { + String longName = longNames.getIfPresent(c9sPath); if (longName != null) { - return Optional.of(new DeflatedFileName(shortName, longName)); + return Optional.of(new DeflatedFileName(c9sPath, longName)); } else { return Optional.empty(); } @@ -101,11 +109,11 @@ public Optional getCached(Path ciphertextFile) { public class DeflatedFileName { - public final String shortName; + public final Path c9sPath; public final String longName; - private DeflatedFileName(String shortName, String longName) { - this.shortName = shortName; + private DeflatedFileName(Path c9sPath, String longName) { + this.c9sPath = c9sPath; this.longName = longName; } @@ -119,15 +127,10 @@ public void persist() { } 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)) { + Path longNameFile = c9sPath.resolve(LONG_NAME_FILE); + Files.createDirectories(c9sPath); + try (WritableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 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)); } } } diff --git a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java index f480b8bf..233a39c3 100644 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java @@ -1,28 +1,23 @@ package org.cryptomator.cryptofs; import com.google.common.base.Strings; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.IOException; -import java.lang.reflect.Field; import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.spi.AbstractInterruptibleChannel; -import java.nio.file.FileSystem; -import java.nio.file.NoSuchFileException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.spi.FileSystemProvider; public class ConflictResolverTest { @@ -31,43 +26,18 @@ public class ConflictResolverTest { private FileNameCryptor filenameCryptor; private ConflictResolver conflictResolver; private String dirId; - private Path testFile; - private Path testFileName; - private Path testDir; - private FileSystem testFileSystem; - private FileSystemProvider testFileSystemProvider; + private Path tmpDir; @BeforeEach - public void setup() { + public void setup(@TempDir Path tmpDir) { + this.tmpDir = tmpDir; this.longFileNameProvider = Mockito.mock(LongFileNameProvider.class); this.cryptor = Mockito.mock(Cryptor.class); this.filenameCryptor = Mockito.mock(FileNameCryptor.class); this.conflictResolver = new ConflictResolver(longFileNameProvider, cryptor); this.dirId = "foo"; - this.testFile = Mockito.mock(Path.class); - this.testFileName = Mockito.mock(Path.class); - this.testDir = Mockito.mock(Path.class); - this.testFileSystem = Mockito.mock(FileSystem.class); - this.testFileSystemProvider = Mockito.mock(FileSystemProvider.class); Mockito.when(cryptor.fileNameCryptor()).thenReturn(filenameCryptor); - Mockito.when(testFile.getParent()).thenReturn(testDir); - Mockito.when(testFile.getFileName()).thenReturn(testFileName); - Mockito.when(testDir.resolve(Mockito.anyString())).then(this::resolveChildOfTestDir); - Mockito.when(testFile.resolveSibling(Mockito.anyString())).then(this::resolveChildOfTestDir); - Mockito.when(testFile.getFileSystem()).thenReturn(testFileSystem); - Mockito.when(testFileSystem.provider()).thenReturn(testFileSystemProvider); - } - - private Path resolveChildOfTestDir(InvocationOnMock invocation) { - Path result = Mockito.mock(Path.class); - Path resultName = Mockito.mock(Path.class); - Mockito.when(result.getFileName()).thenReturn(resultName); - Mockito.when(resultName.toString()).thenReturn(invocation.getArgument(0)); - Mockito.when(result.getParent()).thenReturn(testDir); - Mockito.when(result.getFileSystem()).thenReturn(testFileSystem); - Mockito.when(result.resolveSibling(Mockito.anyString())).then(this::resolveChildOfTestDir); - return result; } private ArgumentMatcher hasFileName(String name) { @@ -89,115 +59,102 @@ private Answer fillBufferWithBytes(byte[] bytes) { }; } - @Test - public void testPassthroughValidBase32NormalFile() throws IOException { - Mockito.when(testFileName.toString()).thenReturn("ABCDEF=="); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verifyNoMoreInteractions(filenameCryptor); - Mockito.verifyNoMoreInteractions(longFileNameProvider); - Assertions.assertEquals(testFile.getFileName().toString(), resolved.getFileName().toString()); - } + @ParameterizedTest + @ValueSource(strings = { + ".DS_Store", + "FooBar==.c9r", + "FooBar==.c9s", + }) + public void testPassthroughNonConflictingFiles(String conflictingFileName) throws IOException { + Path conflictingPath = tmpDir.resolve(conflictingFileName); - @Test - public void testPassthroughInvalidBase32NormalFile() throws IOException { - Mockito.when(testFileName.toString()).thenReturn("ABCDEF== (1)"); - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("ABCDEF=="), Mockito.any())).thenThrow(new AuthenticationFailedException("invalid ciphertext")); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Assertions.assertSame(testFile, resolved); - } + Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - @Test - public void testPassthroughValidBase32LongFile() throws IOException { - Mockito.when(testFileName.toString()).thenReturn("ABCDEF==.lng"); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); + Assertions.assertSame(conflictingPath, result); Mockito.verifyNoMoreInteractions(filenameCryptor); Mockito.verifyNoMoreInteractions(longFileNameProvider); - Assertions.assertEquals(testFile.getFileName().toString(), resolved.getFileName().toString()); } @ParameterizedTest - @ValueSource(strings = {"ABCDEF== (1)", "conflict_ABCDEF=="}) - public void testRenameNormalFile(String conflictingFileName) throws IOException { - String ciphertextName = "ABCDEFGH2345===="; - Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("ABCDEF=="), Mockito.any())).thenReturn("abcdef"); - Mockito.when(filenameCryptor.encryptFilename(Mockito.startsWith("abcdef ("), Mockito.any())).thenReturn(ciphertextName); - Mockito.doThrow(new NoSuchFileException(ciphertextName)).when(testFileSystemProvider).checkAccess(Mockito.argThat(hasFileName(ciphertextName))); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verify(testFileSystemProvider).move(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.argThat(hasFileName(ciphertextName)), Mockito.any()); - Assertions.assertEquals(ciphertextName, resolved.getFileName().toString()); - } + @CsvSource({ + "FooBar== (2).c9r,FooBar==.c9r", + "FooBar== (2).c9s,FooBar==.c9s", + }) + public void testResolveTrivially(String conflictingFileName, String expectedCanonicalName) throws IOException { + Path conflictingPath = tmpDir.resolve(conflictingFileName); + Files.createFile(conflictingPath); - @ParameterizedTest - @ValueSource(strings = {"ABCDEF== (1).lng", "conflict_ABCDEF==.lng"}) - public void testRenameLongFile(String conflictingFileName) throws IOException { - String longCiphertextName = Strings.repeat("ABCDEFGH",Constants.SHORT_NAMES_MAX_LENGTH /8 + 1); - - assert longCiphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH; - Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); - Mockito.when(longFileNameProvider.inflate("ABCDEF==.lng")).thenReturn("FEDCBA=="); - Mockito.when(longFileNameProvider.deflate(longCiphertextName)).thenReturn("FEDCBA==.lng"); - Mockito.when(longFileNameProvider.isDeflated(conflictingFileName)).thenReturn(true); - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("FEDCBA=="), Mockito.any())).thenReturn("fedcba"); - Mockito.when(filenameCryptor.encryptFilename(Mockito.startsWith("fedcba ("), Mockito.any())).thenReturn(longCiphertextName); - Mockito.doThrow(new NoSuchFileException("FEDCBA==.lng")).when(testFileSystemProvider).checkAccess(Mockito.argThat(hasFileName("FEDCBA==.lng"))); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verify(longFileNameProvider).deflate(longCiphertextName); - Mockito.verify(testFileSystemProvider).move(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.argThat(hasFileName("FEDCBA==.lng")), Mockito.any()); - Assertions.assertEquals("FEDCBA==.lng", resolved.getFileName().toString()); + Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); + + Assertions.assertEquals(tmpDir.resolve(expectedCanonicalName), result); + Mockito.verifyNoMoreInteractions(filenameCryptor); + Mockito.verifyNoMoreInteractions(longFileNameProvider); } @ParameterizedTest - @ValueSource(strings = {"0ABCDEF== (1)", "conflict_0ABCDEF=="}) - public void testSilentlyDeleteConflictingDirectoryFileIdenticalToCanonicalFile(String conflictingFileName) throws IOException, ReflectiveOperationException { - Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); - FileChannel canonicalFc = Mockito.mock(FileChannel.class); - FileChannel conflictingFc = Mockito.mock(FileChannel.class); - Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - channelCloseLockField.setAccessible(true); - channelCloseLockField.set(canonicalFc, new Object()); - channelCloseLockField.set(conflictingFc, new Object()); - Mockito.when(testFileSystemProvider.newByteChannel(Mockito.argThat(hasFileName("0ABCDEF==")), Mockito.any(), Mockito.any())).thenReturn(canonicalFc); - Mockito.when(testFileSystemProvider.newByteChannel(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.any(), Mockito.any())).thenReturn(conflictingFc); - Mockito.when(canonicalFc.read(Mockito.any(ByteBuffer.class))).then(fillBufferWithBytes("12345".getBytes())); - Mockito.when(conflictingFc.read(Mockito.any(ByteBuffer.class))).then(fillBufferWithBytes("12345".getBytes())); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verify(testFileSystemProvider).deleteIfExists(Mockito.argThat(hasFileName(conflictingFileName))); - Assertions.assertEquals("0ABCDEF==", resolved.getFileName().toString()); + @CsvSource({ + "FooBar== (2).c9r,FooBar==.c9r,dir.c9r", + "FooBar== (2).c9s,FooBar==.c9s,symlink.c9r", + }) + public void testResolveTriviallyForIdenticalContent(String conflictingFileName, String expectedCanonicalName, String contentFile) throws IOException { + Path conflictingPath = tmpDir.resolve(conflictingFileName); + Path canonicalPath = tmpDir.resolve(expectedCanonicalName); + Files.createDirectory(conflictingPath); + Files.createDirectory(canonicalPath); + Files.write(conflictingPath.resolve(contentFile), new byte[5]); + Files.write(canonicalPath.resolve(contentFile), new byte[5]); + + Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); + + Assertions.assertEquals(canonicalPath, result); + Mockito.verifyNoMoreInteractions(filenameCryptor); + Mockito.verifyNoMoreInteractions(longFileNameProvider); } - @ParameterizedTest - @ValueSource(strings = {"0ABCDEF== (1)", "conflict_0ABCDEF=="}) - public void testSilentlyRenameConflictingDirectoryFileWithMissingCanonicalFile(String conflictingFileName) throws IOException { - Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); - Mockito.doThrow(new NoSuchFileException("0ABCDEF==")).when(testFileSystemProvider).checkAccess(Mockito.argThat(hasFileName("0ABCDEF=="))); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verify(testFileSystemProvider).move(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.argThat(hasFileName("0ABCDEF==")), Mockito.any()); - Assertions.assertEquals("0ABCDEF==", resolved.getFileName().toString()); + @Test + public void testResolveByRenamingRegularFile() throws IOException { + String conflictingName = "FooBar== (2).c9r"; + String canonicalName = "FooBar==.c9r"; + Path conflictingPath = tmpDir.resolve(conflictingName); + Path canonicalPath = tmpDir.resolve(canonicalName); + Files.write(conflictingPath, new byte[3]); + Files.write(canonicalPath, new byte[5]); + + Mockito.when(longFileNameProvider.isDeflated(Mockito.eq(canonicalName))).thenReturn(false); + Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("FooBar=="), Mockito.any())).thenReturn("cleartext.txt"); + Mockito.when(filenameCryptor.encryptFilename(Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"), Mockito.any())).thenReturn("BarFoo=="); + + Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); + + Assertions.assertEquals("BarFoo==.c9r", result.getFileName().toString()); + Assertions.assertFalse(Files.exists(conflictingPath)); + Assertions.assertTrue(Files.exists(result)); } - @ParameterizedTest - @ValueSource(strings = {"0ABCDEF== (1)", "conflict_0ABCDEF=="}) - public void testRenameDirectoryFile(String conflictingFileName) throws IOException, ReflectiveOperationException { - Mockito.when(testFileName.toString()).thenReturn(conflictingFileName); - FileChannel canonicalFc = Mockito.mock(FileChannel.class); - FileChannel conflictingFc = Mockito.mock(FileChannel.class); - Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); - channelCloseLockField.setAccessible(true); - channelCloseLockField.set(canonicalFc, new Object()); - channelCloseLockField.set(conflictingFc, new Object()); - Mockito.when(testFileSystemProvider.newByteChannel(Mockito.argThat(hasFileName("0ABCDEF==")), Mockito.any(), Mockito.any())).thenReturn(canonicalFc); - Mockito.when(testFileSystemProvider.newByteChannel(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.any(), Mockito.any())).thenReturn(conflictingFc); - Mockito.when(canonicalFc.read(Mockito.any(ByteBuffer.class))).then(fillBufferWithBytes("12345".getBytes())); - Mockito.when(conflictingFc.read(Mockito.any(ByteBuffer.class))).then(fillBufferWithBytes("67890".getBytes())); - String ciphertext = "ABCDEFGH2345===="; - String ciphertextName = "0" + ciphertext; - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("ABCDEF=="), Mockito.any())).thenReturn("abcdef"); - Mockito.when(filenameCryptor.encryptFilename(Mockito.startsWith("abcdef ("), Mockito.any())).thenReturn(ciphertext); - Mockito.doThrow(new NoSuchFileException(ciphertextName)).when(testFileSystemProvider).checkAccess(Mockito.argThat(hasFileName(ciphertextName))); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - Mockito.verify(testFileSystemProvider).move(Mockito.argThat(hasFileName(conflictingFileName)), Mockito.argThat(hasFileName(ciphertextName)), Mockito.any()); - Assertions.assertEquals(ciphertextName, resolved.getFileName().toString()); + @Test + public void testResolveByRenamingShortenedFile() throws IOException { + String conflictingName = "FooBar== (2).c9s"; + String canonicalName = "FooBar==.c9s"; + String inflatedName = Strings.repeat("a", Constants.SHORT_NAMES_MAX_LENGTH + 1); + Path conflictingPath = tmpDir.resolve(conflictingName); + Path canonicalPath = tmpDir.resolve(canonicalName); + Files.write(conflictingPath, new byte[3]); + Files.write(canonicalPath, new byte[5]); + + Mockito.when(longFileNameProvider.isDeflated(canonicalName)).thenReturn(true); + Mockito.when(longFileNameProvider.inflate(canonicalPath)).thenReturn(inflatedName); + Mockito.when(filenameCryptor.decryptFilename(Mockito.eq(inflatedName), Mockito.any())).thenReturn("cleartext.txt"); + String resolvedCiphertext = Strings.repeat("b", Constants.SHORT_NAMES_MAX_LENGTH + 1); + Path resolvedInflatedPath = canonicalPath.resolveSibling(resolvedCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX); + Path resolvedDeflatedPath = canonicalPath.resolveSibling("BarFoo==.c9s"); + Mockito.when(filenameCryptor.encryptFilename(Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"), Mockito.any())).thenReturn(resolvedCiphertext); + Mockito.when(longFileNameProvider.deflate(resolvedInflatedPath)).thenReturn(resolvedDeflatedPath); + + Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); + + Assertions.assertEquals(resolvedDeflatedPath, result); + Assertions.assertFalse(Files.exists(conflictingPath)); + Assertions.assertTrue(Files.exists(result)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java index b332392d..ac3556cd 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java @@ -70,7 +70,7 @@ public void testInflateIfNeededWithRegularLongFilename() throws IOException { Files.createFile(ciphertextPath); Path inflatedPath = fileSystem.getPath(inflatedName); when(longFileNameProvider.isDeflated(filename)).thenReturn(true); - when(longFileNameProvider.inflate(filename)).thenReturn(inflatedName); + when(longFileNameProvider.inflate(ciphertextPath)).thenReturn(inflatedName); ProcessedPaths paths = new ProcessedPaths(ciphertextPath); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java index f187d166..3050b513 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java @@ -69,7 +69,7 @@ public void setup() throws IOException { longFileNameProvider = Mockito.mock(LongFileNameProvider.class); conflictResolver = Mockito.mock(ConflictResolver.class); finallyUtil = mock(FinallyUtil.class); - Mockito.when(longFileNameProvider.inflate(Mockito.anyString())).then(invocation -> { + Mockito.when(longFileNameProvider.inflate(Mockito.any())).then(invocation -> { String shortName = invocation.getArgument(0); if (shortName.contains("invalid")) { throw new IOException("invalid shortened name"); diff --git a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java index 344d0f04..d28f7b35 100644 --- a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java @@ -45,55 +45,55 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { } @Test - public void testIsDeflated(@TempDir Path tmpPath) { - Path aPath = tmpPath.resolve("foo"); - Assertions.assertTrue(new LongFileNameProvider(aPath, readonlyFlag).isDeflated("foo.lng")); - Assertions.assertFalse(new LongFileNameProvider(aPath, readonlyFlag).isDeflated("foo.txt")); + public void testIsDeflated() { + Assertions.assertTrue(new LongFileNameProvider(readonlyFlag).isDeflated("foo.c9s")); + Assertions.assertFalse(new LongFileNameProvider(readonlyFlag).isDeflated("foo.txt")); } @Test public void testDeflateAndInflate(@TempDir Path tmpPath) throws IOException { String orig = "longName"; - LongFileNameProvider prov1 = new LongFileNameProvider(tmpPath, readonlyFlag); - String deflated = prov1.deflate(orig); + LongFileNameProvider prov1 = new LongFileNameProvider(readonlyFlag); + Path deflated = prov1.deflate(tmpPath.resolve(orig)); String inflated1 = prov1.inflate(deflated); Assertions.assertEquals(orig, inflated1); Assertions.assertEquals(0, countFiles(tmpPath)); - prov1.getCached(Paths.get(deflated)).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + prov1.getCached(deflated).ifPresent(LongFileNameProvider.DeflatedFileName::persist); Assertions.assertEquals(1, countFiles(tmpPath)); - LongFileNameProvider prov2 = new LongFileNameProvider(tmpPath, readonlyFlag); + LongFileNameProvider prov2 = new LongFileNameProvider(readonlyFlag); String inflated2 = prov2.inflate(deflated); Assertions.assertEquals(orig, inflated2); } @Test - public void testInflateNonExisting(@TempDir Path tmpPath) { - LongFileNameProvider prov = new LongFileNameProvider(tmpPath, readonlyFlag); + public void testInflateNonExisting() { + LongFileNameProvider prov = new LongFileNameProvider(readonlyFlag); Assertions.assertThrows(NoSuchFileException.class, () -> { - prov.inflate("doesNotExist"); + prov.inflate(Paths.get("/does/not/exist")); }); } @Test public void testDeflateMultipleTimes(@TempDir Path tmpPath) { - LongFileNameProvider prov = new LongFileNameProvider(tmpPath, readonlyFlag); - String orig = "longName"; - prov.deflate(orig); - prov.deflate(orig); - prov.deflate(orig); - prov.deflate(orig); + LongFileNameProvider prov = new LongFileNameProvider(readonlyFlag); + Path canonicalFileName = tmpPath.resolve("longName"); + prov.deflate(canonicalFileName); + prov.deflate(canonicalFileName); + prov.deflate(canonicalFileName); + prov.deflate(canonicalFileName); } @Test public void testPerstistCachedFailsOnReadOnlyFileSystems(@TempDir Path tmpPath) { - LongFileNameProvider prov = new LongFileNameProvider(tmpPath, readonlyFlag); + LongFileNameProvider prov = new LongFileNameProvider(readonlyFlag); String orig = "longName"; - String shortened = prov.deflate(orig); - Optional cachedFileName = prov.getCached(Paths.get(shortened)); + Path canonicalFileName = tmpPath.resolve(orig); + Path c9sFile = prov.deflate(canonicalFileName); + Optional cachedFileName = prov.getCached(c9sFile); Assertions.assertTrue(cachedFileName.isPresent()); Mockito.doThrow(new ReadOnlyFileSystemException()).when(readonlyFlag).assertWritable(); From b3cde80ae5327958f327895dcd3a5206c9f44579 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Aug 2019 15:00:18 +0200 Subject: [PATCH 18/62] Preparing CryptoFS for Vault Format 7 --- .../org/cryptomator/cryptofs/Constants.java | 1 + .../cryptofs/CryptoFileSystemImpl.java | 51 ++++--- .../cryptofs/CryptoPathMapper.java | 42 +++--- .../org/cryptomator/cryptofs/Symlinks.java | 33 ++++- .../attr/AbstractCryptoFileAttributeView.java | 4 +- .../cryptofs/attr/AttributeProvider.java | 4 +- .../cryptofs/CryptoFileSystemImplTest.java | 104 ++++++++------ .../cryptofs/CryptoPathMapperTest.java | 108 ++++++++------ .../cryptomator/cryptofs/SymlinksTest.java | 132 +++++++++++------- .../cryptofs/attr/AttributeProviderTest.java | 8 +- .../attr/CryptoDosFileAttributeViewTest.java | 4 +- .../CryptoFileOwnerAttributeViewTest.java | 4 +- .../CryptoPosixFileAttributeViewTest.java | 4 +- 13 files changed, 303 insertions(+), 196 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index 8276accd..4842374c 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -20,6 +20,7 @@ public final class Constants { static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; static final String DIR_FILE_NAME = "dir.c9r"; static final String SYMLINK_FILE_NAME = "symlink.c9r"; + static final String CONTENTS_FILE_NAME = "contents.c9r"; static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index db4fac79..aba95e8c 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -296,9 +296,11 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws throw new NoSuchFileException(cleartextParentDir.toString()); } cryptoPathMapper.assertNonExisting(cleartextDir); - Path ciphertextDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextDir, CiphertextFileType.DIRECTORY); + Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); + Path ciphertextDirFile = ciphertextPath.resolve(Constants.DIR_FILE_NAME); CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); // atomically check for FileAlreadyExists and create otherwise: + Files.createDirectory(ciphertextPath); try (FileChannel channel = FileChannel.open(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), attrs)) { channel.write(ByteBuffer.wrap(ciphertextDir.dirId.getBytes(UTF_8))); } @@ -350,7 +352,7 @@ FileChannel newFileChannel(CryptoPath cleartextPath, Set o } private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath, CiphertextFileType.FILE); + Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath); if (options.createNew() && openCryptoFiles.get(ciphertextPath).isPresent()) { throw new FileAlreadyExistsException(cleartextFilePath.toString()); } else { @@ -369,21 +371,19 @@ void delete(CryptoPath cleartextPath) throws IOException { deleteDirectory(cleartextPath); return; default: - Path ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath, ciphertextFileType); + Path ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); Files.deleteIfExists(ciphertextFilePath); return; } } private void deleteDirectory(CryptoPath cleartextPath) throws IOException { + Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path; - Path ciphertextDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY); + Path ciphertextDirFile = ciphertextPath.resolve(Constants.DIR_FILE_NAME); try { ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath); - if (!Files.deleteIfExists(ciphertextDirFile)) { - // should not happen. Nevertheless this is a valid state, so no big deal... - LOG.warn("Successfully deleted dir {}, but didn't find corresponding dir file {}", ciphertextDir, ciphertextDirFile); - } + Files.walkFileTree(ciphertextPath, DeletingFileVisitor.INSTANCE); cryptoPathMapper.invalidatePathMapping(cleartextPath); dirIdProvider.delete(ciphertextDirFile); } catch (NoSuchFileException e) { @@ -421,13 +421,12 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) { - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.SYMLINK); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK); + Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new); Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile); Files.copy(ciphertextSourceFile, ciphertextTargetFile, resolvedOptions); deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); - } else { CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource); CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget); @@ -437,8 +436,8 @@ 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); + Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile); Files.copy(ciphertextSourceFile, ciphertextTargetFile, options); deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); @@ -446,7 +445,7 @@ private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // DIRECTORY (non-recursive as per contract): - Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.DIRECTORY); + Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); if (Files.notExists(ciphertextTargetDirFile)) { // create new: Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetDirFile); @@ -526,8 +525,8 @@ void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // according to Files.move() JavaDoc: // "the symbolic link itself, not the target of the link, is moved" - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.SYMLINK); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK); + Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) { Files.move(ciphertextSourceFile, ciphertextTargetFile, options); longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); @@ -538,8 +537,8 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // While moving a file, it is possible to keep the channels open. In order to make this work // we need to re-map the OpenCryptoFile entry. - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE); + Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) { Files.move(ciphertextSourceFile, ciphertextTargetFile, options); longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); @@ -550,12 +549,12 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // Since we only rename the directory file, all ciphertext paths of subresources stay the same. // Hence there is no need to re-map OpenCryptoFile entries. - Path ciphertextSourceDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.DIRECTORY); - Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.DIRECTORY); + Path ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + Path ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // try to move, don't replace: - Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options); - longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.move(ciphertextSource, ciphertextTarget, options); + longFileNameProvider.getCached(ciphertextTarget).ifPresent(LongFileNameProvider.DeflatedFileName::persist); } else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { // replace atomically (impossible): assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); @@ -564,7 +563,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge // move and replace (if dir is empty): assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); assert !ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE); - if (Files.exists(ciphertextTargetDirFile)) { + if (Files.exists(ciphertextTarget)) { Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; try (DirectoryStream ds = Files.newDirectoryStream(ciphertextTargetDir)) { if (ds.iterator().hasNext()) { @@ -573,10 +572,10 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge } Files.delete(ciphertextTargetDir); } - Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options); - longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.move(ciphertextSource, ciphertextTarget, options); + longFileNameProvider.getCached(ciphertextTarget).ifPresent(LongFileNameProvider.DeflatedFileName::persist); } - dirIdProvider.move(ciphertextSourceDirFile, ciphertextTargetDirFile); + dirIdProvider.move(ciphertextSource.resolve(Constants.DIR_FILE_NAME), ciphertextTarget.resolve(Constants.DIR_FILE_NAME)); cryptoPathMapper.invalidatePathMapping(cleartextSource); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 499fde9d..8d8402c6 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; @@ -65,8 +66,11 @@ public class CryptoPathMapper { */ public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException { try { - CiphertextFileType type = getCiphertextFileType(cleartextPath); - throw new FileAlreadyExistsException(cleartextPath.toString(), null, "For this path there is already a " + type.name()); + Path ciphertextPath = getCiphertextFilePath(cleartextPath); + BasicFileAttributes attr = Files.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attr != null) { + throw new FileAlreadyExistsException(cleartextPath.toString()); + } } catch (NoSuchFileException e) { // good! } @@ -78,32 +82,32 @@ public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExists * @throws NoSuchFileException If no node exists at the given path for any known type * @throws IOException */ - @Deprecated public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws NoSuchFileException, IOException { CryptoPath parentPath = cleartextPath.getParent(); if (parentPath == null) { return CiphertextFileType.DIRECTORY; // ROOT } else { - CiphertextDirectory parent = getCiphertextDir(parentPath); - String cleartextName = cleartextPath.getFileName().toString(); - NoSuchFileException notFound = new NoSuchFileException(cleartextPath.toString()); - for (CiphertextFileType type : CiphertextFileType.values()) { - String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parent.dirId, cleartextName)); - Path ciphertextPath = parent.path.resolve(ciphertextName); - try { - // readattr is the fastest way of checking if a file exists. Doing so in this loop is still - // 1-2 orders of magnitude faster than iterating over directory contents - Files.readAttributes(ciphertextPath, BasicFileAttributes.class); - return type; - } catch (NoSuchFileException e) { - notFound.addSuppressed(e); + Path ciphertextPath = getCiphertextFilePath(cleartextPath); + BasicFileAttributes attr = Files.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attr.isRegularFile()) { + return CiphertextFileType.FILE; + } else if (attr.isDirectory()) { + Path symlinkFilePath = ciphertextPath.resolve(Constants.SYMLINK_FILE_NAME); + Path dirFilePath = ciphertextPath.resolve(Constants.DIR_FILE_NAME); + Path contentsFilePath = ciphertextPath.resolve(Constants.CONTENTS_FILE_NAME); + if (Files.exists(dirFilePath, LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.DIRECTORY; + } else if (Files.exists(symlinkFilePath, LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.SYMLINK; + } else if (Files.exists(contentsFilePath, LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.FILE; } } - throw notFound; + throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath); } } - public Path getCiphertextFilePath(CryptoPath cleartextPath, CiphertextFileType type) throws IOException { + public Path getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { CryptoPath parentPath = cleartextPath.getParent(); if (parentPath == null) { throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath); @@ -135,7 +139,7 @@ public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOE } else { try { return ciphertextDirectories.get(cleartextPath, () -> { - Path dirIdFile = getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY); + Path dirIdFile = getCiphertextFilePath(cleartextPath).resolve(Constants.DIR_FILE_NAME); return resolveDirectory(dirIdFile); }); } catch (ExecutionException e) { diff --git a/src/main/java/org/cryptomator/cryptofs/Symlinks.java b/src/main/java/org/cryptomator/cryptofs/Symlinks.java index 93b837e6..92971377 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -7,10 +7,13 @@ import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.file.FileSystemLoopException; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.NotLinkException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.util.EnumSet; import java.util.HashSet; @@ -39,16 +42,18 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib if (target.toString().length() > Constants.MAX_SYMLINK_LENGTH) { throw new IOException("path length limit exceeded."); } - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK); + Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).resolve(Constants.SYMLINK_FILE_NAME); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag); ByteBuffer content = UTF_8.encode(target.toString()); + Files.createDirectory(ciphertextSymlinkFile.getParent()); openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content); longFileNameProvider.getCached(ciphertextSymlinkFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); } public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException { - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK); + Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).resolve(Constants.SYMLINK_FILE_NAME); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.READ), readonlyFlag); + assertIsSymlink(cleartextPath, ciphertextSymlinkFile); try { ByteBuffer content = openCryptoFiles.readCiphertextFile(ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH); return cleartextPath.getFileSystem().getPath(UTF_8.decode(content).toString()); @@ -57,6 +62,30 @@ public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException } } + /** + * @param cleartextPath + * @param ciphertextSymlinkFile + * @throws NoSuchFileException If the dir containing {@value Constants#SYMLINK_FILE_NAME} does not exist. + * @throws NotLinkException If the resource represented by cleartextPath exists but {@value Constants#SYMLINK_FILE_NAME} does not. + * @throws IOException In case of any other I/O error + */ + private void assertIsSymlink(CryptoPath cleartextPath, Path ciphertextSymlinkFile) throws IOException { + Path parentDir = ciphertextSymlinkFile.getParent(); + BasicFileAttributes parentAttr = Files.readAttributes(parentDir, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (parentAttr.isDirectory()) { + try { + BasicFileAttributes fileAttr = Files.readAttributes(ciphertextSymlinkFile, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (!fileAttr.isRegularFile()) { + throw new NotLinkException(cleartextPath.toString(), null, "File exists but is not a symlink."); + } + } catch (NoSuchFileException e) { + throw new NotLinkException(cleartextPath.toString(), null, "File exists but is not a symlink."); + } + } else { + throw new NotLinkException(cleartextPath.toString(), null, "File exists but is not a symlink."); + } + } + public CryptoPath resolveRecursively(CryptoPath cleartextPath) throws IOException { return resolveRecursively(new HashSet<>(), cleartextPath); } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 15095c7b..130d53e8 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -53,7 +53,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException { switch (type) { case SYMLINK: if (ArrayUtils.contains(linkOptions, LinkOption.NOFOLLOW_LINKS)) { - return pathMapper.getCiphertextFilePath(path, type); + return pathMapper.getCiphertextFilePath(path); } else { CryptoPath resolved = symlinks.resolveRecursively(path); return getCiphertextPath(resolved); @@ -61,7 +61,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException { case DIRECTORY: return pathMapper.getCiphertextDir(path).path; default: - return pathMapper.getCiphertextFilePath(path, type); + return pathMapper.getCiphertextFilePath(path); } } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java index 7f9e0c9e..0d5f14e8 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java @@ -66,7 +66,7 @@ public A readAttributes(CryptoPath cleartextPath switch (ciphertextFileType) { case SYMLINK: { if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) { - Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath, ciphertextFileType); + Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath); return readAttributes(ciphertextFileType, ciphertextPath, type); } else { CryptoPath resolved = symlinks.resolveRecursively(cleartextPath); @@ -78,7 +78,7 @@ public A readAttributes(CryptoPath cleartextPath return readAttributes(ciphertextFileType, ciphertextPath, type); } case FILE: { - Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath, ciphertextFileType); + Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath); return readAttributes(ciphertextFileType, ciphertextPath, type); } default: diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 60460c81..fb3bcc22 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -31,6 +31,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; +import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -49,6 +50,7 @@ import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.UserPrincipal; import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; @@ -338,43 +340,61 @@ public void testNewWatchServiceThrowsUnsupportedOperationException() throws IOEx public class Delete { private final CryptoPath cleartextPath = mock(CryptoPath.class, "cleartext"); - private final Path ciphertextFilePath = mock(Path.class, "ciphertextFile"); - private final Path ciphertextDirFilePath = mock(Path.class, "ciphertextDirFile"); - private final Path ciphertextDirPath = mock(Path.class, "ciphertextDir"); + private final Path ciphertextPath = 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 ciphertextDirPath = mock(Path.class, "d/FF/FF/"); private final FileSystem physicalFs = mock(FileSystem.class); private final FileSystemProvider physicalFsProv = mock(FileSystemProvider.class); + private final BasicFileAttributes ciphertextPathAttr = mock(BasicFileAttributes.class); + private final BasicFileAttributes ciphertextDirFilePathAttr = mock(BasicFileAttributes.class); @BeforeEach public void setup() throws IOException { - when(ciphertextFilePath.getFileSystem()).thenReturn(physicalFs); - when(ciphertextDirFilePath.getFileSystem()).thenReturn(physicalFs); - when(ciphertextDirPath.getFileSystem()).thenReturn(physicalFs); when(physicalFs.provider()).thenReturn(physicalFsProv); - when(cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextFilePath); - when(cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY)).thenReturn(ciphertextDirFilePath); + when(ciphertextPath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextDirPath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextDirFilePath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFilePath); + when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(cleartextPath)).thenReturn(new CiphertextDirectory("foo", ciphertextDirPath)); + when(physicalFsProv.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); + when(physicalFsProv.readAttributes(ciphertextDirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextDirFilePathAttr); + } @Test public void testDeleteExistingFile() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(true); + when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(true); inTest.delete(cleartextPath); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).deleteIfExists(ciphertextFilePath); + verify(physicalFsProv).deleteIfExists(ciphertextPath); } @Test public void testDeleteExistingDirectory() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false); + when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(false); + when(ciphertextPathAttr.isDirectory()).thenReturn(true); + when(physicalFsProv.newDirectoryStream(Mockito.eq(ciphertextPath), Mockito.any())).thenReturn(new DirectoryStream() { + @Override + public Iterator iterator() { + return Arrays.asList(ciphertextDirFilePath).iterator(); + } + + @Override + public void close() { + // no-op + } + }); inTest.delete(cleartextPath); verify(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath); verify(readonlyFlag).assertWritable(); verify(physicalFsProv).deleteIfExists(ciphertextDirFilePath); + verify(physicalFsProv).deleteIfExists(ciphertextPath); verify(dirIdProvider).delete(ciphertextDirFilePath); verify(cryptoPathMapper).invalidatePathMapping(cleartextPath); } @@ -391,7 +411,7 @@ public void testDeleteNonExistingFileOrDir() throws IOException { @Test public void testDeleteNonEmptyDir() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false); + when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(false); Mockito.doThrow(new DirectoryNotEmptyException("ciphertextDir")).when(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { @@ -408,17 +428,19 @@ public class CopyAndMove { private final CryptoPath sourceLinkTarget = mock(CryptoPath.class, "sourceLinkTarget"); private final CryptoPath cleartextDestination = mock(CryptoPath.class, "cleartextDestination"); private final CryptoPath destinationLinkTarget = mock(CryptoPath.class, "destinationLinkTarget"); - private final Path ciphertextSourceFile = mock(Path.class, "ciphertextSourceFile"); - private final Path ciphertextSourceDirFile = mock(Path.class, "ciphertextSourceDirFile"); - private final Path ciphertextSourceDir = mock(Path.class, "ciphertextSourceDir"); - private final Path ciphertextDestinationFile = mock(Path.class, "ciphertextDestinationFile"); - private final Path ciphertextDestinationDirFile = mock(Path.class, "ciphertextDestinationDirFile"); - private final Path ciphertextDestinationDir = mock(Path.class, "ciphertextDestinationDir"); + private final Path ciphertextSourceFile = mock(Path.class, "d/00/00/source.c9r"); + private final Path ciphertextSourceDirFile = mock(Path.class, "d/00/00/source.c9r/dir.c9r"); + private final Path ciphertextSourceDir = mock(Path.class, "d/00/SOURCE/"); + private final Path ciphertextDestinationFile = mock(Path.class, "d/00/00/dest.c9r"); + private final Path ciphertextDestinationDirFile = mock(Path.class, "d/00/00/dest.c9r/dir.c9r"); + private final Path ciphertextDestinationDir = mock(Path.class, "d/00/DEST/"); private final FileSystem physicalFs = mock(FileSystem.class); private final FileSystemProvider physicalFsProv = mock(FileSystemProvider.class); @BeforeEach public void setup() throws IOException { + when(ciphertextSourceFile.resolve("dir.c9r")).thenReturn(ciphertextSourceDirFile); + when(ciphertextDestinationFile.resolve("dir.c9r")).thenReturn(ciphertextDestinationDirFile); when(ciphertextSourceFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDir.getFileSystem()).thenReturn(physicalFs); @@ -426,20 +448,16 @@ public void setup() throws IOException { when(ciphertextDestinationDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextDestinationDir.getFileSystem()).thenReturn(physicalFs); when(physicalFs.provider()).thenReturn(physicalFsProv); - when(cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.DIRECTORY)).thenReturn(ciphertextSourceDirFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.SYMLINK)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination, CiphertextFileType.FILE)).thenReturn(ciphertextDestinationFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination, CiphertextFileType.DIRECTORY)).thenReturn(ciphertextDestinationDirFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination, CiphertextFileType.SYMLINK)).thenReturn(ciphertextDestinationFile); + when(cryptoPathMapper.getCiphertextFilePath(cleartextSource)).thenReturn(ciphertextSourceFile); + when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination)).thenReturn(ciphertextDestinationFile); when(cryptoPathMapper.getCiphertextDir(cleartextSource)).thenReturn(new CiphertextDirectory("foo", ciphertextSourceDir)); when(cryptoPathMapper.getCiphertextDir(cleartextDestination)).thenReturn(new CiphertextDirectory("bar", ciphertextDestinationDir)); when(symlinks.resolveRecursively(cleartextSource)).thenReturn(sourceLinkTarget); when(symlinks.resolveRecursively(cleartextDestination)).thenReturn(destinationLinkTarget); when(cryptoPathMapper.getCiphertextFileType(sourceLinkTarget)).thenReturn(CiphertextFileType.FILE); when(cryptoPathMapper.getCiphertextFileType(destinationLinkTarget)).thenReturn(CiphertextFileType.FILE); - when(cryptoPathMapper.getCiphertextFilePath(sourceLinkTarget, CiphertextFileType.FILE)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget, CiphertextFileType.FILE)).thenReturn(ciphertextDestinationFile); + when(cryptoPathMapper.getCiphertextFilePath(sourceLinkTarget)).thenReturn(ciphertextSourceFile); + when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget)).thenReturn(ciphertextDestinationFile); } @Nested @@ -517,7 +535,7 @@ public void moveDirectoryDontReplaceExisting() throws IOException { inTest.move(cleartextSource, cleartextDestination, option1, option2); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).move(ciphertextSourceDirFile, ciphertextDestinationDirFile, option1, option2); + verify(physicalFsProv).move(ciphertextSourceFile, ciphertextDestinationFile, option1, option2); verify(dirIdProvider).move(ciphertextSourceDirFile, ciphertextDestinationDirFile); verify(cryptoPathMapper).invalidatePathMapping(cleartextSource); } @@ -537,7 +555,7 @@ public void moveDirectoryReplaceExisting() throws IOException { verify(readonlyFlag).assertWritable(); verify(physicalFsProv).delete(ciphertextDestinationDir); - verify(physicalFsProv).move(ciphertextSourceDirFile, ciphertextDestinationDirFile, StandardCopyOption.REPLACE_EXISTING); + verify(physicalFsProv).move(ciphertextSourceFile, ciphertextDestinationFile, StandardCopyOption.REPLACE_EXISTING); verify(dirIdProvider).move(ciphertextSourceDirFile, ciphertextDestinationDirFile); verify(cryptoPathMapper).invalidatePathMapping(cleartextSource); } @@ -671,7 +689,7 @@ public void copyFile() throws IOException { public void copyDirectory() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); - Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationDirFile); + Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile); inTest.copy(cleartextSource, cleartextDestination); @@ -706,7 +724,7 @@ public void copyDirectoryReplaceExisting() throws IOException { public void moveDirectoryCopyBasicAttributes() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); - Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationDirFile); + Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile); when(fileStore.supportedFileAttributeViewTypes()).thenReturn(EnumSet.of(AttributeViewType.BASIC)); FileTime lastModifiedTime = FileTime.from(1, TimeUnit.HOURS); FileTime lastAccessTime = FileTime.from(2, TimeUnit.HOURS); @@ -729,7 +747,7 @@ public void moveDirectoryCopyBasicAttributes() throws IOException { public void moveDirectoryCopyFileOwnerAttributes() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); - Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationDirFile); + Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile); when(fileStore.supportedFileAttributeViewTypes()).thenReturn(EnumSet.of(AttributeViewType.OWNER)); UserPrincipal owner = mock(UserPrincipal.class); FileOwnerAttributeView srcAttrsView = mock(FileOwnerAttributeView.class); @@ -749,7 +767,7 @@ public void moveDirectoryCopyFileOwnerAttributes() throws IOException { public void moveDirectoryCopyPosixAttributes() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); - Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationDirFile); + Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile); when(fileStore.supportedFileAttributeViewTypes()).thenReturn(EnumSet.of(AttributeViewType.POSIX)); GroupPrincipal group = mock(GroupPrincipal.class); Set permissions = mock(Set.class); @@ -771,7 +789,7 @@ public void moveDirectoryCopyPosixAttributes() throws IOException { public void moveDirectoryCopyDosAttributes() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class); - Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationDirFile); + Mockito.doThrow(new NoSuchFileException("ciphertextDestinationDirFile")).when(physicalFsProv).checkAccess(ciphertextDestinationFile); when(fileStore.supportedFileAttributeViewTypes()).thenReturn(EnumSet.of(AttributeViewType.DOS)); DosFileAttributes srcAttrs = mock(DosFileAttributes.class); DosFileAttributeView dstAttrView = mock(DosFileAttributeView.class); @@ -886,16 +904,19 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio CryptoPath path = mock(CryptoPath.class, "path"); CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); - Path ciphertextDirFile = mock(Path.class, "ciphertextDirFile"); - Path ciphertextDirPath = mock(Path.class, "ciphertextDir"); + Path ciphertextPath = mock(Path.class, "d/00/00/path.c9r"); + Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); + Path ciphertextDirPath = mock(Path.class, "d/FF/FF/"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); when(path.getParent()).thenReturn(parent); - when(cryptoPathMapper.getCiphertextFilePath(path, CiphertextFileType.DIRECTORY)).thenReturn(ciphertextDirFile); + when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); + when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath)); when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); + when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem); when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel); @@ -911,16 +932,19 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro CryptoPath path = mock(CryptoPath.class, "path"); CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); - Path ciphertextDirFile = mock(Path.class, "ciphertextDirFile"); - Path ciphertextDirPath = mock(Path.class, "ciphertextDir"); + Path ciphertextPath = mock(Path.class, "d/00/00/path.c9r"); + Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); + Path ciphertextDirPath = mock(Path.class, "d/FF/FF/"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); when(path.getParent()).thenReturn(parent); - when(cryptoPathMapper.getCiphertextFilePath(path, CiphertextFileType.DIRECTORY)).thenReturn(ciphertextDirFile); + when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); + when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath)); when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); + when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem); when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel); @@ -1215,7 +1239,7 @@ public void setAttributeOnFile() throws IOException { Path ciphertextFilePath = mock(Path.class); when(cryptoPathMapper.getCiphertextFileType(path)).thenReturn(CiphertextFileType.FILE); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory("foo", ciphertextDirPath)); - when(cryptoPathMapper.getCiphertextFilePath(path, CiphertextFileType.FILE)).thenReturn(ciphertextFilePath); + when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextFilePath); doThrow(new NoSuchFileException("")).when(provider).checkAccess(ciphertextDirPath); inTest.setAttribute(path, name, value); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 9edb5283..bfad1d6b 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.file.FileSystem; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; @@ -77,11 +78,13 @@ public void testPathEncryptionForFoo() throws IOException { Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); Path d0000 = Mockito.mock(Path.class, "d/00/00"); - Path d00000oof = Mockito.mock(Path.class, "d/00/00/0oof"); + Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r"); + Path d0000oofdir = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r"); Mockito.when(d00.resolve("00")).thenReturn(d0000); - Mockito.when(d0000.resolve("0oof")).thenReturn(d00000oof); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); Path d0001 = Mockito.mock(Path.class); @@ -99,19 +102,23 @@ public void testPathEncryptionForFooBar() throws IOException { Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); Path d0000 = Mockito.mock(Path.class, "d/00/00"); - Path d00000oof = Mockito.mock(Path.class, "d/00/00/0oof"); + Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r"); + Path d0000oofdir = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r"); Mockito.when(d00.resolve("00")).thenReturn(d0000); - Mockito.when(d0000.resolve("0oof")).thenReturn(d00000oof); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); Path d0001 = Mockito.mock(Path.class, "d/00/01"); - Path d00010rab = Mockito.mock(Path.class, "d/00/01/0rab"); + Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r"); + Path d0000rabdir = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r"); Mockito.when(d00.resolve("01")).thenReturn(d0001); - Mockito.when(d0001.resolve("0rab")).thenReturn(d00010rab); + Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); + Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); Mockito.when(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); - Mockito.when(dirIdProvider.load(d00010rab)).thenReturn("2"); + Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2"); Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002"); Path d0002 = Mockito.mock(Path.class); @@ -129,43 +136,45 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); Path d0000 = Mockito.mock(Path.class, "d/00/00"); - Path d00000oof = Mockito.mock(Path.class, "d/00/00/0oof"); + Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r"); + Path d0000oofdir = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r"); Mockito.when(d00.resolve("00")).thenReturn(d0000); - Mockito.when(d0000.resolve("0oof")).thenReturn(d00000oof); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); Path d0001 = Mockito.mock(Path.class, "d/00/01"); - Path d00010rab = Mockito.mock(Path.class, "d/00/01/0rab"); + Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r"); + Path d0000rabdir = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r"); Mockito.when(d00.resolve("01")).thenReturn(d0001); - Mockito.when(d0001.resolve("0rab")).thenReturn(d00010rab); + Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); + Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); Mockito.when(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); - Mockito.when(dirIdProvider.load(d00010rab)).thenReturn("2"); + Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2"); Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002"); Path d0002 = Mockito.mock(Path.class, "d/00/02"); - Path d0002zab = Mockito.mock(Path.class, "d/00/02/zab"); - Path d00020zab = Mockito.mock(Path.class, "d/00/02/0zab"); + Path d0002zab = Mockito.mock(Path.class, "d/00/02/zab.c9r"); Mockito.when(d00.resolve("02")).thenReturn(d0002); - Mockito.when(d0002.resolve("zab")).thenReturn(d0002zab); - Mockito.when(d0002.resolve("0zab")).thenReturn(d00020zab); + Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab); Mockito.when(fileNameCryptor.encryptFilename("baz", "2".getBytes())).thenReturn("zab"); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); - Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz"), CiphertextFileType.FILE); + Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")); Assertions.assertEquals(d0002zab, path); - Path path2 = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz"), CiphertextFileType.DIRECTORY); - Assertions.assertEquals(d00020zab, path2); } @Nested public class GetCiphertextFileType { private FileSystemProvider underlyingFileSystemProvider; - private Path d0000CIPHER; - private Path d00000CIPHER; - private Path d00001SCIPHER; + private Path c9rPath; + private Path dirFilePath; + private Path symlinkFilePath; + private Path contentsFilePath; + private BasicFileAttributes c9rAttrs; @BeforeEach public void setup() throws IOException { @@ -180,15 +189,21 @@ public void setup() throws IOException { Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); Mockito.when(fileNameCryptor.encryptFilename("CLEAR", "".getBytes())).thenReturn("CIPHER"); - d0000CIPHER = Mockito.mock(Path.class, "d/00/00/CIPHER"); - d00000CIPHER = Mockito.mock(Path.class, "d/00/00/0CIPHER"); - d00001SCIPHER = Mockito.mock(Path.class, "d/00/00/1SCIPHER"); - Mockito.when(d0000.resolve("CIPHER")).thenReturn(d0000CIPHER); - Mockito.when(d0000.resolve("0CIPHER")).thenReturn(d00000CIPHER); - Mockito.when(d0000.resolve("1SCIPHER")).thenReturn(d00001SCIPHER); - Mockito.when(d0000CIPHER.getFileSystem()).thenReturn(underlyingFileSystem); - Mockito.when(d00000CIPHER.getFileSystem()).thenReturn(underlyingFileSystem); - Mockito.when(d00001SCIPHER.getFileSystem()).thenReturn(underlyingFileSystem); + c9rPath = Mockito.mock(Path.class, "d/00/00/CIPHER.c9r"); + c9rAttrs = Mockito.mock(BasicFileAttributes.class, "attributes for d/00/00/CIPHER.c9r"); + Mockito.when(d0000.resolve("CIPHER.c9r")).thenReturn(c9rPath); + Mockito.when(c9rPath.getFileSystem()).thenReturn(underlyingFileSystem); + + dirFilePath = Mockito.mock(Path.class, "d/00/00/CIPHER.c9r/dir.c9r"); + symlinkFilePath = Mockito.mock(Path.class, "d/00/00/CIPHER.c9r/symlink.c9r"); + contentsFilePath = Mockito.mock(Path.class, "d/00/00/CIPHER.c9r/contents.c9r"); + Mockito.when(c9rPath.resolve("dir.c9r")).thenReturn(dirFilePath); + Mockito.when(c9rPath.resolve("symlink.c9r")).thenReturn(symlinkFilePath); + Mockito.when(c9rPath.resolve("contents.c9r")).thenReturn(contentsFilePath); + Mockito.when(dirFilePath.getFileSystem()).thenReturn(underlyingFileSystem); + Mockito.when(symlinkFilePath.getFileSystem()).thenReturn(underlyingFileSystem); + Mockito.when(contentsFilePath.getFileSystem()).thenReturn(underlyingFileSystem); + } @@ -201,9 +216,7 @@ public void testGetCiphertextFileTypeOfRootPath() throws IOException { @Test public void testGetCiphertextFileTypeForNonexistingFile() throws IOException { - Mockito.when(underlyingFileSystemProvider.readAttributes(d0000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00001SCIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); @@ -215,9 +228,8 @@ public void testGetCiphertextFileTypeForNonexistingFile() throws IOException { @Test public void testGetCiphertextFileTypeForFile() throws IOException { - Mockito.when(underlyingFileSystemProvider.readAttributes(d0000CIPHER, BasicFileAttributes.class)).thenReturn(Mockito.mock(BasicFileAttributes.class)); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00001SCIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); + Mockito.when(c9rAttrs.isRegularFile()).thenReturn(true); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); @@ -228,9 +240,11 @@ public void testGetCiphertextFileTypeForFile() throws IOException { @Test public void testGetCiphertextFileTypeForDirectory() throws IOException { - Mockito.when(underlyingFileSystemProvider.readAttributes(d0000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00000CIPHER, BasicFileAttributes.class)).thenReturn(Mockito.mock(BasicFileAttributes.class)); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00001SCIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); + Mockito.when(c9rAttrs.isDirectory()).thenReturn(true); + Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class)); + Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); @@ -241,9 +255,11 @@ public void testGetCiphertextFileTypeForDirectory() throws IOException { @Test public void testGetCiphertextFileTypeForSymlink() throws IOException { - Mockito.when(underlyingFileSystemProvider.readAttributes(d0000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00000CIPHER, BasicFileAttributes.class)).thenThrow(NoSuchFileException.class); - Mockito.when(underlyingFileSystemProvider.readAttributes(d00001SCIPHER, BasicFileAttributes.class)).thenReturn(Mockito.mock(BasicFileAttributes.class)); + Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs); + Mockito.when(c9rAttrs.isDirectory()).thenReturn(true); + Mockito.when(underlyingFileSystemProvider.readAttributes(dirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); + Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class)); + Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); diff --git a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java index 89e3d0d8..2a6c74df 100644 --- a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java +++ b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java @@ -1,6 +1,5 @@ package org.cryptomator.cryptofs; -import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -11,9 +10,13 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; import java.nio.file.FileSystemLoopException; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.spi.FileSystemProvider; public class SymlinksTest { @@ -21,11 +24,8 @@ public class SymlinksTest { private final LongFileNameProvider longFileNameProvider = Mockito.mock(LongFileNameProvider.class); private final OpenCryptoFiles openCryptoFiles = Mockito.mock(OpenCryptoFiles.class); private final ReadonlyFlag readonlyFlag = Mockito.mock(ReadonlyFlag.class); - - private final CryptoFileSystemImpl fs = Mockito.mock(CryptoFileSystemImpl.class, "cryptoFs"); - private final CryptoPath cleartextPath = Mockito.mock(CryptoPath.class, "cleartextPath"); - private final OpenCryptoFile ciphertextFile = Mockito.mock(OpenCryptoFile.class); - private final Path ciphertextPath = Mockito.mock(Path.class, "ciphertextPath"); + private final FileSystem underlyingFs = Mockito.mock(FileSystem.class); + private final FileSystemProvider underlyingFsProvider = Mockito.mock(FileSystemProvider.class); private Symlinks inTest; @@ -33,32 +33,57 @@ public class SymlinksTest { public void setup() throws IOException { inTest = new Symlinks(cryptoPathMapper, longFileNameProvider, openCryptoFiles, readonlyFlag); - Mockito.when(cleartextPath.getFileSystem()).thenReturn(fs); - Mockito.when(openCryptoFiles.getOrCreate(ciphertextPath)).thenReturn(ciphertextFile); + Mockito.when(underlyingFs.provider()).thenReturn(underlyingFsProvider); + } + + private Path mockExistingSymlink(CryptoPath cleartextPath) throws IOException { + Path ciphertextPath = Mockito.mock(Path.class); + Path symlinkFilePath = Mockito.mock(Path.class); + BasicFileAttributes ciphertextPathAttr = Mockito.mock(BasicFileAttributes.class); + BasicFileAttributes symlinkFilePathAttr = Mockito.mock(BasicFileAttributes.class); + Mockito.when(ciphertextPath.resolve("symlink.c9r")).thenReturn(symlinkFilePath); + Mockito.when(symlinkFilePath.getParent()).thenReturn(ciphertextPath); + Mockito.when(ciphertextPath.getFileSystem()).thenReturn(underlyingFs); + Mockito.when(symlinkFilePath.getFileSystem()).thenReturn(underlyingFs); + Mockito.when(underlyingFsProvider.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); + Mockito.when(underlyingFsProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(symlinkFilePathAttr); + Mockito.when(ciphertextPathAttr.isDirectory()).thenReturn(true); + Mockito.when(symlinkFilePathAttr.isRegularFile()).thenReturn(true); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + return ciphertextPath; } @Test public void testCreateSymbolicLink() throws IOException { + CryptoPath cleartextPath = Mockito.mock(CryptoPath.class); Path target = Mockito.mock(Path.class, "targetPath"); + Path ciphertextPath = mockExistingSymlink(cleartextPath); + Path symlinkFilePath = ciphertextPath.resolve("symlink.c9r"); Mockito.doNothing().when(cryptoPathMapper).assertNonExisting(cleartextPath); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath); Mockito.when(target.toString()).thenReturn("/symlink/target/path"); inTest.createSymbolicLink(cleartextPath, target); ArgumentCaptor bytesWritten = ArgumentCaptor.forClass(ByteBuffer.class); - Mockito.verify(openCryptoFiles).writeCiphertextFile(Mockito.eq(ciphertextPath), Mockito.any(), bytesWritten.capture()); + Mockito.verify(underlyingFsProvider).createDirectory(Mockito.eq(ciphertextPath), Mockito.any()); + Mockito.verify(openCryptoFiles).writeCiphertextFile(Mockito.eq(symlinkFilePath), Mockito.any(), bytesWritten.capture()); Assertions.assertEquals("/symlink/target/path", StandardCharsets.UTF_8.decode(bytesWritten.getValue()).toString()); } @Test public void testReadSymbolicLink() throws IOException { + CryptoPath cleartextPath = Mockito.mock(CryptoPath.class); + CryptoFileSystemImpl cleartextFs = Mockito.mock(CryptoFileSystemImpl.class); + Mockito.when(cleartextPath.getFileSystem()).thenReturn(cleartextFs); + String targetPath = "/symlink/target/path2"; CryptoPath resolvedTargetPath = Mockito.mock(CryptoPath.class, "resolvedTargetPath"); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode(targetPath)); + Path ciphertextPath = mockExistingSymlink(cleartextPath); + Path symlinkFilePath = ciphertextPath.resolve("symlink.c9r"); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(symlinkFilePath), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode(targetPath)); - Mockito.when(fs.getPath(targetPath)).thenReturn(resolvedTargetPath); + Mockito.when(cleartextFs.getPath(targetPath)).thenReturn(resolvedTargetPath); CryptoPath read = inTest.readSymbolicLink(cleartextPath); @@ -67,33 +92,36 @@ public void testReadSymbolicLink() throws IOException { @Test public void testResolveRecursivelyForRegularFile() throws IOException { - CryptoPath cleartextPath1 = Mockito.mock(CryptoPath.class); - Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.FILE); + CryptoPath cleartextPath = Mockito.mock(CryptoPath.class); + Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - CryptoPath resolved = inTest.resolveRecursively(cleartextPath1); + CryptoPath resolved = inTest.resolveRecursively(cleartextPath); - Assertions.assertSame(cleartextPath1, resolved); + Assertions.assertSame(cleartextPath, resolved); } @Test public void testResolveRecursively() throws IOException { + CryptoFileSystemImpl cleartextFs = Mockito.mock(CryptoFileSystemImpl.class); CryptoPath cleartextPath1 = Mockito.mock(CryptoPath.class); CryptoPath cleartextPath2 = Mockito.mock(CryptoPath.class); CryptoPath cleartextPath3 = Mockito.mock(CryptoPath.class); - Path ciphertextPath1 = Mockito.mock(Path.class); - Path ciphertextPath2 = Mockito.mock(Path.class); - Mockito.when(cleartextPath1.getFileSystem()).thenReturn(fs); - Mockito.when(cleartextPath2.getFileSystem()).thenReturn(fs); - Mockito.when(cleartextPath3.getFileSystem()).thenReturn(fs); + Path ciphertextPath1 = mockExistingSymlink(cleartextPath1); + Path ciphertextPath2 = mockExistingSymlink(cleartextPath2); + Path ciphertextSymlinkPath1 = ciphertextPath1.resolve("symlink.c9r"); + Path ciphertextSymlinkPath2 = ciphertextPath2.resolve("symlink.c9r"); + Mockito.when(cleartextPath1.getFileSystem()).thenReturn(cleartextFs); + Mockito.when(cleartextPath2.getFileSystem()).thenReturn(cleartextFs); + Mockito.when(cleartextPath3.getFileSystem()).thenReturn(cleartextFs); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath3)).thenReturn(CiphertextFileType.FILE); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath1); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath2); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); - Mockito.when(fs.getPath("file2")).thenReturn(cleartextPath2); - Mockito.when(fs.getPath("file3")).thenReturn(cleartextPath3); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2)).thenReturn(ciphertextPath2); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); + Mockito.when(cleartextFs.getPath("file2")).thenReturn(cleartextPath2); + Mockito.when(cleartextFs.getPath("file3")).thenReturn(cleartextPath3); CryptoPath resolved = inTest.resolveRecursively(cleartextPath1); @@ -102,16 +130,18 @@ public void testResolveRecursively() throws IOException { @Test public void testResolveRecursivelyWithNonExistingTarget() throws IOException { + CryptoFileSystemImpl cleartextFs = Mockito.mock(CryptoFileSystemImpl.class); CryptoPath cleartextPath1 = Mockito.mock(CryptoPath.class); CryptoPath cleartextPath2 = Mockito.mock(CryptoPath.class); - Path ciphertextPath1 = Mockito.mock(Path.class); - Mockito.when(cleartextPath1.getFileSystem()).thenReturn(fs); - Mockito.when(cleartextPath2.getFileSystem()).thenReturn(fs); + Path ciphertextPath1 = mockExistingSymlink(cleartextPath1); + Path ciphertextSymlinkPath1 = ciphertextPath1.resolve("symlink.c9r"); + Mockito.when(cleartextPath1.getFileSystem()).thenReturn(cleartextFs); + Mockito.when(cleartextPath2.getFileSystem()).thenReturn(cleartextFs); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenThrow(new NoSuchFileException("cleartextPath2")); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath1); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); - Mockito.when(fs.getPath("file2")).thenReturn(cleartextPath2); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); + Mockito.when(cleartextFs.getPath("file2")).thenReturn(cleartextPath2); CryptoPath resolved = inTest.resolveRecursively(cleartextPath1); @@ -120,27 +150,31 @@ public void testResolveRecursivelyWithNonExistingTarget() throws IOException { @Test public void testResolveRecursivelyWithLoop() throws IOException { + CryptoFileSystemImpl cleartextFs = Mockito.mock(CryptoFileSystemImpl.class); CryptoPath cleartextPath1 = Mockito.mock(CryptoPath.class); CryptoPath cleartextPath2 = Mockito.mock(CryptoPath.class); CryptoPath cleartextPath3 = Mockito.mock(CryptoPath.class); - Path ciphertextPath1 = Mockito.mock(Path.class); - Path ciphertextPath2 = Mockito.mock(Path.class); - Path ciphertextPath3 = Mockito.mock(Path.class); - Mockito.when(cleartextPath1.getFileSystem()).thenReturn(fs); - Mockito.when(cleartextPath2.getFileSystem()).thenReturn(fs); - Mockito.when(cleartextPath3.getFileSystem()).thenReturn(fs); + Path ciphertextPath1 = mockExistingSymlink(cleartextPath1); + Path ciphertextPath2 = mockExistingSymlink(cleartextPath2); + Path ciphertextPath3 = mockExistingSymlink(cleartextPath3); + Path ciphertextSymlinkPath1 = ciphertextPath1.resolve("symlink.c9r"); + Path ciphertextSymlinkPath2 = ciphertextPath2.resolve("symlink.c9r"); + Path ciphertextSymlinkPath3 = ciphertextPath3.resolve("symlink.c9r"); + Mockito.when(cleartextPath1.getFileSystem()).thenReturn(cleartextFs); + Mockito.when(cleartextPath2.getFileSystem()).thenReturn(cleartextFs); + Mockito.when(cleartextPath3.getFileSystem()).thenReturn(cleartextFs); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath3)).thenReturn(CiphertextFileType.SYMLINK); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath1); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath2); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath3, CiphertextFileType.SYMLINK)).thenReturn(ciphertextPath3); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); - Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextPath3), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file1")); - Mockito.when(fs.getPath("file2")).thenReturn(cleartextPath2); - Mockito.when(fs.getPath("file3")).thenReturn(cleartextPath3); - Mockito.when(fs.getPath("file1")).thenReturn(cleartextPath1); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2)).thenReturn(ciphertextPath2); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath3)).thenReturn(ciphertextPath3); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); + Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath3), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file1")); + Mockito.when(cleartextFs.getPath("file2")).thenReturn(cleartextPath2); + Mockito.when(cleartextFs.getPath("file3")).thenReturn(cleartextPath3); + Mockito.when(cleartextFs.getPath("file1")).thenReturn(cleartextPath1); Assertions.assertThrows(FileSystemLoopException.class, () -> { inTest.resolveRecursively(cleartextPath1); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java index aefa0c92..6af1fd92 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java @@ -64,7 +64,7 @@ public void setup() throws IOException { Mockito.when(provider.readAttributes(Mockito.same(ciphertextFilePath), Mockito.same(DosFileAttributes.class), Mockito.any())).thenReturn(dosAttr); Mockito.when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); // needed for cleartxt file size calculation FileHeaderCryptor fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); @@ -87,7 +87,7 @@ public class Files { @BeforeEach public void setup() throws IOException { Mockito.when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); } @Test @@ -154,7 +154,7 @@ public void setup() throws IOException { @Test public void testReadBasicAttributesNoFollow() throws IOException { - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); AttributeProvider prov = new AttributeProvider(cryptor, pathMapper, openCryptoFiles, fileSystemProperties, symlinks); BasicFileAttributes attr = prov.readAttributes(cleartextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); @@ -167,7 +167,7 @@ public void testReadBasicAttributesOfTarget() throws IOException { CryptoPath targetPath = Mockito.mock(CryptoPath.class, "targetPath"); Mockito.when(symlinks.resolveRecursively(cleartextPath)).thenReturn(targetPath); Mockito.when(pathMapper.getCiphertextFileType(targetPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(targetPath, CiphertextFileType.FILE)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(targetPath)).thenReturn(ciphertextFilePath); AttributeProvider prov = new AttributeProvider(cryptor, pathMapper, openCryptoFiles, fileSystemProperties, symlinks); BasicFileAttributes attr = prov.readAttributes(cleartextPath, BasicFileAttributes.class); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java index ab4beb31..f2486a58 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java @@ -57,9 +57,9 @@ public void setup() throws IOException { when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); - when(pathMapper.getCiphertextFilePath(link, CiphertextFileType.SYMLINK)).thenReturn(linkCiphertextPath); + when(pathMapper.getCiphertextFilePath(link)).thenReturn(linkCiphertextPath); when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextPath); + when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); inTest = new CryptoDosFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, fileAttributeProvider, readonlyFlag); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java index 851c8de1..1b993d9e 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -51,9 +51,9 @@ public void setup() throws IOException { when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); - when(pathMapper.getCiphertextFilePath(link, CiphertextFileType.SYMLINK)).thenReturn(linkCiphertextPath); + when(pathMapper.getCiphertextFilePath(link)).thenReturn(linkCiphertextPath); when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextPath); + when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java index e82fb700..4a5026a5 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java @@ -62,9 +62,9 @@ public void setUp() throws IOException { when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); - when(pathMapper.getCiphertextFilePath(link, CiphertextFileType.SYMLINK)).thenReturn(linkCiphertextPath); + when(pathMapper.getCiphertextFilePath(link)).thenReturn(linkCiphertextPath); when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(pathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextPath); + when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); inTest = new CryptoPosixFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, fileAttributeProvider, readonlyFlag); } From bdd033f5472164b3cf5f947e25f577f7f0bbab27 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Aug 2019 16:44:50 +0200 Subject: [PATCH 19/62] encode names using base64 (work in progress) [ci skip] --- .../cryptofs/ConflictResolver.java | 4 +-- .../cryptofs/CryptoDirectoryStream.java | 7 ++-- .../cryptofs/CryptoPathMapper.java | 3 +- .../cryptofs/EncryptedNamePattern.java | 8 ++--- .../cryptofs/ConflictResolverTest.java | 4 +-- .../cryptofs/CryptoDirectoryStreamTest.java | 17 ++++----- .../cryptofs/CryptoPathMapperTest.java | 14 ++++---- .../cryptofs/EncryptedNamePatternTest.java | 36 +++++++++++-------- 8 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java index 6273a0bf..8746f9cd 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java @@ -121,7 +121,7 @@ private Path resolveConflict(Path conflictingPath, Path canonicalPath, String di private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId) throws IOException { assert Files.exists(canonicalPath); try { - String cleartext = cryptor.fileNameCryptor().decryptFilename(ciphertext, dirId.getBytes(StandardCharsets.UTF_8)); + String cleartext = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId.getBytes(StandardCharsets.UTF_8)); Path alternativePath = canonicalPath; for (int i = 1; Files.exists(alternativePath); i++) { String alternativeCleartext = cleartext + " (Conflict " + i + ")"; @@ -138,7 +138,7 @@ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, Str return resolved; } catch (AuthenticationFailedException e) { // not decryptable, no need to resolve any kind of conflict - LOG.info("Found valid Base32 string, which is an unauthentic ciphertext: {}", conflictingPath); + LOG.info("Found valid Base64 string, which is an unauthentic ciphertext: {}", conflictingPath); return conflictingPath; } } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java index 088bc08b..00d02862 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; @@ -27,8 +28,6 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; - class CryptoDirectoryStream implements DirectoryStream { private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class); @@ -111,11 +110,11 @@ ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { } private ProcessedPaths decrypt(ProcessedPaths paths) { - Optional ciphertextName = encryptedNamePattern.extractEncryptedNameFromStart(paths.getInflatedPath()); + Optional ciphertextName = encryptedNamePattern.extractEncryptedName(paths.getInflatedPath()); if (ciphertextName.isPresent()) { String ciphertext = ciphertextName.get(); try { - String cleartext = filenameCryptor.decryptFilename(ciphertext, directoryId.getBytes(StandardCharsets.UTF_8)); + String cleartext = filenameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, directoryId.getBytes(StandardCharsets.UTF_8)); return paths.withCleartextPath(cleartextDir.resolve(cleartext)); } catch (AuthenticationFailedException e) { LOG.warn(paths.getInflatedPath() + " not decryptable due to an unauthentic ciphertext."); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 8d8402c6..4e56bbea 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -13,6 +13,7 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.Cryptor; import javax.inject.Inject; @@ -124,7 +125,7 @@ public Path getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { } private String getCiphertextFileName(DirIdAndName dirIdAndName) { - return cryptor.fileNameCryptor().encryptFilename(dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; + return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; } public void invalidatePathMapping(CryptoPath cleartextPath) { diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java index 554705c8..b0da9ba3 100644 --- a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java +++ b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java @@ -10,17 +10,17 @@ @Singleton class EncryptedNamePattern { - private static final Pattern BASE32_PATTERN_AT_START_OF_NAME = Pattern.compile("^(0|1[A-Z0-9])?(([A-Z2-7]{8})*[A-Z2-7=]{8})"); + private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}"); @Inject public EncryptedNamePattern() { } - public Optional extractEncryptedNameFromStart(Path ciphertextFile) { + public Optional extractEncryptedName(Path ciphertextFile) { String name = ciphertextFile.getFileName().toString(); - Matcher matcher = BASE32_PATTERN_AT_START_OF_NAME.matcher(name); + Matcher matcher = BASE64_PATTERN.matcher(name); if (matcher.find(0)) { - return Optional.of(matcher.group(2)); + return Optional.of(matcher.group()); } else { return Optional.empty(); } diff --git a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java index 233a39c3..89e29215 100644 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java @@ -121,7 +121,7 @@ public void testResolveByRenamingRegularFile() throws IOException { Files.write(canonicalPath, new byte[5]); Mockito.when(longFileNameProvider.isDeflated(Mockito.eq(canonicalName))).thenReturn(false); - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq("FooBar=="), Mockito.any())).thenReturn("cleartext.txt"); + Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq("FooBar=="), Mockito.any())).thenReturn("cleartext.txt"); Mockito.when(filenameCryptor.encryptFilename(Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"), Mockito.any())).thenReturn("BarFoo=="); Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); @@ -143,7 +143,7 @@ public void testResolveByRenamingShortenedFile() throws IOException { Mockito.when(longFileNameProvider.isDeflated(canonicalName)).thenReturn(true); Mockito.when(longFileNameProvider.inflate(canonicalPath)).thenReturn(inflatedName); - Mockito.when(filenameCryptor.decryptFilename(Mockito.eq(inflatedName), Mockito.any())).thenReturn("cleartext.txt"); + Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq(inflatedName), Mockito.any())).thenReturn("cleartext.txt"); String resolvedCiphertext = Strings.repeat("b", Constants.SHORT_NAMES_MAX_LENGTH + 1); Path resolvedInflatedPath = canonicalPath.resolveSibling(resolvedCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX); Path resolvedDeflatedPath = canonicalPath.resolveSibling("BarFoo==.c9s"); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java index 3050b513..369c578f 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; @@ -106,14 +107,14 @@ public void testDirListing() throws IOException { Path cleartextPath = Paths.get("/foo/bar"); List ciphertextFileNames = new ArrayList<>(); - ciphertextFileNames.add(filenameCryptor.encryptFilename("one", "foo".getBytes())); - ciphertextFileNames.add(filenameCryptor.encryptFilename("two", "foo".getBytes()) + "_conflict"); - ciphertextFileNames.add("0" + filenameCryptor.encryptFilename("three", "foo".getBytes())); - ciphertextFileNames.add("0invalidDirectory"); - ciphertextFileNames.add("0noDirectory"); - ciphertextFileNames.add("invalidLongName.lng"); - ciphertextFileNames.add(filenameCryptor.encryptFilename("four", "foo".getBytes()) + ".lng"); - ciphertextFileNames.add(filenameCryptor.encryptFilename("invalid", "bar".getBytes())); + ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(), "one", "foo".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"two", "foo".getBytes()) + " (conflict)" + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add("?" + filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"three", "foo".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add("0invalidDirectory" + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add("0noDirectory" + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add("invalidLongName.lng" + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"four", "foo".getBytes()) + ".lng" + Constants.CRYPTOMATOR_FILE_SUFFIX); + ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"invalid", "bar".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); ciphertextFileNames.add("alsoInvalid"); Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator()); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index bfad1d6b..af8f63d2 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -83,7 +83,7 @@ public void testPathEncryptionForFoo() throws IOException { Mockito.when(d00.resolve("00")).thenReturn(d0000); Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); - Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(),Mockito.eq("foo"), Mockito.any())).thenReturn("oof"); Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); @@ -107,7 +107,7 @@ public void testPathEncryptionForFooBar() throws IOException { Mockito.when(d00.resolve("00")).thenReturn(d0000); Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); - Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof"); Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); @@ -117,7 +117,7 @@ public void testPathEncryptionForFooBar() throws IOException { Mockito.when(d00.resolve("01")).thenReturn(d0001); Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); - Mockito.when(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab"); Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2"); Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002"); @@ -141,7 +141,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(d00.resolve("00")).thenReturn(d0000); Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); - Mockito.when(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof"); Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1"); Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); @@ -151,7 +151,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(d00.resolve("01")).thenReturn(d0001); Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); - Mockito.when(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab"); Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2"); Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002"); @@ -159,7 +159,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Path d0002zab = Mockito.mock(Path.class, "d/00/02/zab.c9r"); Mockito.when(d00.resolve("02")).thenReturn(d0002); Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab); - Mockito.when(fileNameCryptor.encryptFilename("baz", "2".getBytes())).thenReturn("zab"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("baz"), Mockito.any())).thenReturn("zab"); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")); @@ -188,7 +188,7 @@ public void setup() throws IOException { Mockito.when(d00.resolve("00")).thenReturn(d0000); Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); - Mockito.when(fileNameCryptor.encryptFilename("CLEAR", "".getBytes())).thenReturn("CIPHER"); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("CLEAR"), Mockito.any())).thenReturn("CIPHER"); c9rPath = Mockito.mock(Path.class, "d/00/00/CIPHER.c9r"); c9rAttrs = Mockito.mock(BasicFileAttributes.class, "attributes for d/00/00/CIPHER.c9r"); Mockito.when(d0000.resolve("CIPHER.c9r")).thenReturn(c9rPath); diff --git a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java index 74b3918d..0fd2371e 100644 --- a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java +++ b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java @@ -1,30 +1,36 @@ package org.cryptomator.cryptofs; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; public class EncryptedNamePatternTest { - - private static final String ENCRYPTED_NAME = "ALKDUEEH2445375AUZEJFEFA"; - private static final Path PATH_WITHOUT_ENCRYPTED_NAME = Paths.get("foo.txt"); - private static final Path PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX = Paths.get("foo" + ENCRYPTED_NAME + ".txt"); - + private EncryptedNamePattern inTest = new EncryptedNamePattern(); - @Test - public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfNoEncryptedNameIsPresent() { - Optional result = inTest.extractEncryptedNameFromStart(PATH_WITHOUT_ENCRYPTED_NAME); - - Assertions.assertFalse(result.isPresent()); + @ParameterizedTest + @ValueSource(strings = { + "aaaaBBBBcccc0000----__==", + "?aaaaBBBBcccc0000----__==", + "aaaaBBBBcccc0000----__== (conflict)", + "?aaaaBBBBcccc0000----__== (conflict)", + }) + public void testValidCiphertextNames(String name) { + Optional result = inTest.extractEncryptedName(Paths.get(name)); + + Assertions.assertTrue(result.isPresent()); } - @Test - public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfEncryptedNameIsPresentAfterStart() { - Optional result = inTest.extractEncryptedNameFromStart(PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX); + @ParameterizedTest + @ValueSource(strings = { + "tooShort", + "aaaaBBBB====0000----__==", + }) + public void testInvalidCiphertextNames(String name) { + Optional result = inTest.extractEncryptedName(Paths.get(name)); Assertions.assertFalse(result.isPresent()); } From 5b3b2a76882ef1bc53526260b1d7a222b767a5f3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 22 Aug 2019 22:24:50 +0200 Subject: [PATCH 20/62] pattern is supposed to be greedy --- .../cryptomator/cryptofs/migration/v7/FilePathMigration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index a774537d..3456b6c7 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -26,7 +26,7 @@ class FilePathMigration { private static final String OLD_SHORTENED_FILENAME_SUFFIX = ".lng"; private static final Pattern OLD_SHORTENED_FILENAME_PATTERN = Pattern.compile("[A-Z2-7]{32}"); - private static final Pattern OLD_CANONICAL_FILENAME_PATTERN = Pattern.compile("(0|1S)?([A-Z2-7]{8})*?[A-Z2-7=]{8}"); + private static final Pattern OLD_CANONICAL_FILENAME_PATTERN = Pattern.compile("(0|1S)?([A-Z2-7]{8})*[A-Z2-7=]{8}"); private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); private static final int SHORTENING_THRESHOLD = 222; // see calculations in https://github.com/cryptomator/cryptofs/issues/60 From a623c38c1308d6cfe821cabebddf036a865b2d2d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 23 Aug 2019 11:46:04 +0200 Subject: [PATCH 21/62] Fixed deletion of symlinks and long-named files, added tests --- .../cryptofs/CryptoFileSystemImpl.java | 2 +- ...yptoFileSystemProviderIntegrationTest.java | 57 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index aba95e8c..3957e578 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -372,7 +372,7 @@ void delete(CryptoPath cleartextPath) throws IOException { return; default: Path ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); - Files.deleteIfExists(ciphertextFilePath); + Files.walkFileTree(ciphertextFilePath, DeletingFileVisitor.INSTANCE); return; } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java index a8172d4b..9dd4411b 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java @@ -260,16 +260,53 @@ public void testReadFromSymlink() throws IOException { @Test @Order(8) - @DisplayName("mkdir '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen'") - public void testLongFileNames() throws IOException { - Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen"); + @DisplayName("rm /link") + public void testRemoveSymlink() throws IOException { + Path link = fs1.getPath("/link"); + Assumptions.assumeTrue(Files.isSymbolicLink(link)); + Files.delete(link); + } + + @Test + @Order(9) + @DisplayName("mkdir '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'") + public void testCreateDirWithLongName() throws IOException { + Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet"); Files.createDirectory(longNamePath); Assertions.assertTrue(Files.isDirectory(longNamePath)); MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath)); } @Test - @Order(9) + @Order(10) + @DisplayName("rm -r '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'") + public void testRemoveDirWithLongName() throws IOException { + Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet"); + Files.delete(longNamePath); + Assertions.assertTrue(Files.notExists(longNamePath)); + } + + @Test + @Order(11) + @DisplayName("touch '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'") + public void testCreateFileWithLongName() throws IOException { + Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet"); + Files.createFile(longNamePath); + Assertions.assertTrue(Files.isRegularFile(longNamePath)); + MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath)); + } + + @Test + @Order(12) + @DisplayName("rm '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'") + public void testRemoveFileWithLongName() throws IOException { + Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet"); + Files.delete(longNamePath); + Assertions.assertTrue(Files.notExists(longNamePath)); + } + + @Test + @Order(13) @DisplayName("cp fs1:/foo fs2:/bar") public void testCopyFileAcrossFilesystem() throws IOException { Path file1 = fs1.getPath("/foo"); @@ -283,7 +320,7 @@ public void testCopyFileAcrossFilesystem() throws IOException { } @Test - @Order(10) + @Order(14) @DisplayName("echo 'goodbye world' > /foo") public void testWriteToFile() throws IOException { Path file1 = fs1.getPath("/foo"); @@ -292,7 +329,7 @@ public void testWriteToFile() throws IOException { } @Test - @Order(11) + @Order(15) @DisplayName("cp -f fs1:/foo fs2:/bar") public void testCopyFileAcrossFilesystemReplaceExisting() throws IOException { Path file1 = fs1.getPath("/foo"); @@ -306,7 +343,7 @@ public void testCopyFileAcrossFilesystemReplaceExisting() throws IOException { } @Test - @Order(12) + @Order(16) @DisplayName("readattr /attributes.txt") public void testLazinessOfFileAttributeViews() throws IOException { Path file = fs1.getPath("/attributes.txt"); @@ -331,7 +368,7 @@ public void testLazinessOfFileAttributeViews() throws IOException { } @Test - @Order(13) + @Order(17) @DisplayName("ln -s /linked/targetY /links/linkX") public void testSymbolicLinks() throws IOException { Path linksDir = fs1.getPath("/links"); @@ -370,7 +407,7 @@ public void testSymbolicLinks() throws IOException { } @Test - @Order(13) + @Order(18) @DisplayName("mv -f fs1:/foo fs2:/baz") public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException { Path file1 = fs1.getPath("/foo"); @@ -490,7 +527,7 @@ public void setup(@TempDir Path tmpDir) throws IOException { Path pathToVault = tmpDir.resolve("vaultDir1"); Files.createDirectories(pathToVault); CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd"); - fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build()); + fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build()); } @Test From 8e0aba121fed8cbe4c3d296fbe4bcaab31179336 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 3 Sep 2019 14:01:39 +0200 Subject: [PATCH 22/62] moved constants, fixed bug which made nodes disappear if the ciphertext started with `0`. --- .../org/cryptomator/cryptofs/ConflictResolver.java | 8 ++++---- .../java/org/cryptomator/cryptofs/Constants.java | 2 ++ .../cryptofs/CryptoDirectoryStream.java | 4 ++-- .../cryptomator/cryptofs/LongFileNameProvider.java | 14 ++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java index 8746f9cd..980661d9 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java @@ -22,11 +22,11 @@ import java.util.regex.Pattern; import static org.cryptomator.cryptofs.Constants.CRYPTOMATOR_FILE_SUFFIX; +import static org.cryptomator.cryptofs.Constants.DEFLATED_FILE_SUFFIX; import static org.cryptomator.cryptofs.Constants.DIR_FILE_NAME; import static org.cryptomator.cryptofs.Constants.MAX_SYMLINK_LENGTH; import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; import static org.cryptomator.cryptofs.Constants.SYMLINK_FILE_NAME; -import static org.cryptomator.cryptofs.LongFileNameProvider.SHORTENED_NAME_EXT; @CryptoFileSystemScoped class ConflictResolver { @@ -59,9 +59,9 @@ public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throw final String basename; final String extension; - if (ciphertextFileName.endsWith(SHORTENED_NAME_EXT)) { - basename = StringUtils.removeEnd(ciphertextFileName, SHORTENED_NAME_EXT); - extension = SHORTENED_NAME_EXT; + if (ciphertextFileName.endsWith(DEFLATED_FILE_SUFFIX)) { + basename = StringUtils.removeEnd(ciphertextFileName, DEFLATED_FILE_SUFFIX); + extension = DEFLATED_FILE_SUFFIX; } else if (ciphertextFileName.endsWith(CRYPTOMATOR_FILE_SUFFIX)) { basename = StringUtils.removeEnd(ciphertextFileName, CRYPTOMATOR_FILE_SUFFIX); extension = CRYPTOMATOR_FILE_SUFFIX; diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index 4842374c..fdff5f39 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -18,9 +18,11 @@ public final class Constants { static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 static final String ROOT_DIR_ID = ""; static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; + static final String DEFLATED_FILE_SUFFIX = ".c9s"; static final String DIR_FILE_NAME = "dir.c9r"; static final String SYMLINK_FILE_NAME = "symlink.c9r"; static final String CONTENTS_FILE_NAME = "contents.c9r"; + static final String INFLATED_FILE_NAME = "name.c9s"; static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java index 00d02862..d1d8d694 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java @@ -136,8 +136,8 @@ private boolean passesPlausibilityChecks(ProcessedPaths paths) { } private boolean isBrokenDirectoryFile(ProcessedPaths paths) { - Path potentialDirectoryFile = paths.getCiphertextPath(); - if (paths.getInflatedPath().getFileName().toString().startsWith(CiphertextFileType.DIRECTORY.getPrefix())) { + Path potentialDirectoryFile = paths.getCiphertextPath().resolve(Constants.DIR_FILE_NAME); + if (Files.isRegularFile(potentialDirectoryFile)) { final Path dirPath; try { dirPath = cryptoPathMapper.resolveDirectory(potentialDirectoryFile).path; diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index 68770416..b3367ca9 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -22,16 +22,16 @@ import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; 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; +import static org.cryptomator.cryptofs.Constants.DEFLATED_FILE_SUFFIX; +import static org.cryptomator.cryptofs.Constants.INFLATED_FILE_NAME; @CryptoFileSystemScoped class LongFileNameProvider { @@ -39,8 +39,6 @@ class LongFileNameProvider { private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name. private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); private static final Duration MAX_CACHE_AGE = Duration.ofMinutes(1); - public static final String SHORTENED_NAME_EXT = ".c9s"; - private static final String LONG_NAME_FILE = "name.c9s"; private final ReadonlyFlag readonlyFlag; private final LoadingCache longNames; @@ -55,7 +53,7 @@ private class Loader extends CacheLoader { @Override public String load(Path c9sPath) throws IOException { - Path longNameFile = c9sPath.resolve(LONG_NAME_FILE); + Path longNameFile = c9sPath.resolve(INFLATED_FILE_NAME); try (SeekableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.READ)) { if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { throw new UninflatableFileException("Unexpectedly large file: " + longNameFile); @@ -71,7 +69,7 @@ public String load(Path c9sPath) throws IOException { } public boolean isDeflated(String possiblyDeflatedFileName) { - return possiblyDeflatedFileName.endsWith(SHORTENED_NAME_EXT); + return possiblyDeflatedFileName.endsWith(DEFLATED_FILE_SUFFIX); } public String inflate(Path c9sPath) throws IOException { @@ -87,7 +85,7 @@ public Path deflate(Path canonicalFileName) { String longFileName = canonicalFileName.getFileName().toString(); byte[] longFileNameBytes = longFileName.getBytes(UTF_8); byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); - String shortName = BASE64.encode(hash) + SHORTENED_NAME_EXT; + String shortName = BASE64.encode(hash) + DEFLATED_FILE_SUFFIX; Path result = canonicalFileName.resolveSibling(shortName); String cachedLongName = longNames.getIfPresent(shortName); if (cachedLongName == null) { @@ -127,7 +125,7 @@ public void persist() { } private void persistInternal() throws IOException { - Path longNameFile = c9sPath.resolve(LONG_NAME_FILE); + Path longNameFile = c9sPath.resolve(INFLATED_FILE_NAME); Files.createDirectories(c9sPath); try (WritableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { ch.write(UTF_8.encode(longName)); From d6d05da83f138dc1d032be87b131b45b3d1b1b6a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 3 Sep 2019 14:02:15 +0200 Subject: [PATCH 23/62] removed `m` directory and some dead code --- .../cryptofs/CiphertextFileType.java | 32 ++----------------- .../org/cryptomator/cryptofs/Constants.java | 1 - .../cryptofs/CryptoFileSystemProvider.java | 2 -- .../cryptofs/CiphertextFileTypeTest.java | 30 ----------------- ...ptyCiphertextDirectoryIntegrationTest.java | 12 +++---- 5 files changed, 8 insertions(+), 69 deletions(-) delete mode 100644 src/test/java/org/cryptomator/cryptofs/CiphertextFileTypeTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java index 02cd3e2f..f4a034b8 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java @@ -1,36 +1,10 @@ package org.cryptomator.cryptofs; -import java.util.Arrays; -import java.util.function.Predicate; -import java.util.stream.Stream; - /** * Filename prefix as defined issue 38. */ public enum CiphertextFileType { - FILE(""), DIRECTORY("0"), SYMLINK("1S"); - - private final String prefix; - - CiphertextFileType(String prefix) { - this.prefix = prefix; - } - - @Deprecated - public String getPrefix() { - return prefix; - } - - public boolean isTypeOfFile(String filename) { - return filename.startsWith(prefix); - } - - public static CiphertextFileType forFileName(String filename) { - return nonTrivialValues().filter(type -> type.isTypeOfFile(filename)).findAny().orElse(CiphertextFileType.FILE); - } - - public static Stream nonTrivialValues() { - Predicate isTrivial = FILE::equals; - return Arrays.stream(values()).filter(isTrivial.negate()); - } + FILE, + DIRECTORY, + SYMLINK; } diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java index fdff5f39..e0c3a147 100644 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/Constants.java @@ -14,7 +14,6 @@ public final class Constants { public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; static final String DATA_DIR_NAME = "d"; - @Deprecated static final String METADATA_DIR_NAME = "m"; static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 static final String ROOT_DIR_ID = ""; static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 22237704..4e06d511 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -166,8 +166,6 @@ public static void initialize(Path pathToVault, String masterkeyFilename, byte[] String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID); Path rootDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(rootDirHash.substring(0, 2)).resolve(rootDirHash.substring(2)); Files.createDirectories(rootDirPath); - // create "m": - Files.createDirectory(pathToVault.resolve(Constants.METADATA_DIR_NAME)); } assert containsVault(pathToVault, masterkeyFilename); } diff --git a/src/test/java/org/cryptomator/cryptofs/CiphertextFileTypeTest.java b/src/test/java/org/cryptomator/cryptofs/CiphertextFileTypeTest.java deleted file mode 100644 index 8588b74c..00000000 --- a/src/test/java/org/cryptomator/cryptofs/CiphertextFileTypeTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.cryptomator.cryptofs; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -public class CiphertextFileTypeTest { - - @Test - public void testNonTrivialValues() { - Set result = CiphertextFileType.nonTrivialValues().collect(Collectors.toSet()); - Assertions.assertFalse(result.contains(CiphertextFileType.FILE)); - Assertions.assertTrue(result.containsAll(Arrays.asList(CiphertextFileType.DIRECTORY, CiphertextFileType.SYMLINK))); - } - - @DisplayName("CiphertextFileType.forFileName(...)") - @ParameterizedTest(name = "{0}") - @CsvSource(value = {"FOO, ''", "0FOO, 0", "1SFOO, 1S", "1XFOO, ''"}) - public void testNonTrivialValues(String filename, String expectedPrefix) { - CiphertextFileType result = CiphertextFileType.forFileName(filename); - Assertions.assertEquals(expectedPrefix, result.getPrefix()); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index dadc6853..d7aebaa9 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -35,13 +35,11 @@ public class DeleteNonEmptyCiphertextDirectoryIntegrationTest { private static Path pathToVault; - private static Path mDir; private static FileSystem fileSystem; @BeforeAll public static void setupClass(@TempDir Path tmpDir) throws IOException { pathToVault = tmpDir.resolve("vault"); - mDir = pathToVault.resolve("m"); Files.createDirectory(pathToVault); fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build()); } @@ -84,7 +82,8 @@ public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IO Files.createDirectory(cleartextDirectory); Path ciphertextDirectory = firstEmptyCiphertextDirectory(); - createFile(ciphertextDirectory, "HHEZJURE.lng", new byte[] {65}); + Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s"); + createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65}); Files.delete(cleartextDirectory); } @@ -95,10 +94,9 @@ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() thro Files.createDirectory(cleartextDirectory); Path ciphertextDirectory = firstEmptyCiphertextDirectory(); - createFile(ciphertextDirectory, "HHEZJURE.lng", new byte[] {65}); - Path mSubdir = mDir.resolve("HH").resolve("EZ"); - Files.createDirectories(mSubdir); - createFile(mSubdir, "HHEZJURE.lng", "0HHEZJUREHHEZJUREHHEZJURE".getBytes()); + Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s"); + createFile(longNameDir, Constants.INFLATED_FILE_NAME, "HHEZJUREHHEZJUREHHEZJURE".getBytes()); + createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65}); Files.delete(cleartextDirectory); } From 0268f1e3583c4da853cf4bfb359fbdbe91d23740 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 3 Sep 2019 14:23:43 +0200 Subject: [PATCH 24/62] code cleanup --- .../org/cryptomator/cryptofs/CryptoFileSystemImpl.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index 3957e578..f0a3b3f2 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -366,19 +366,18 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp void delete(CryptoPath cleartextPath) throws IOException { readonlyFlag.assertWritable(); CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath); + Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); switch (ciphertextFileType) { case DIRECTORY: - deleteDirectory(cleartextPath); + deleteDirectory(cleartextPath, ciphertextPath); return; default: - Path ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); - Files.walkFileTree(ciphertextFilePath, DeletingFileVisitor.INSTANCE); + Files.walkFileTree(ciphertextPath, DeletingFileVisitor.INSTANCE); return; } } - private void deleteDirectory(CryptoPath cleartextPath) throws IOException { - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); + private void deleteDirectory(CryptoPath cleartextPath, Path ciphertextPath) throws IOException { Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path; Path ciphertextDirFile = ciphertextPath.resolve(Constants.DIR_FILE_NAME); try { From 43d7772babc6e8e87ca4333d4e220ec3061ef618 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 3 Sep 2019 15:56:48 +0200 Subject: [PATCH 25/62] CryptoPathMapper will now return a container for the various ciphertext paths. --- .../cryptofs/CiphertextFilePath.java | 56 +++++++++++ .../cryptofs/CryptoFileSystemImpl.java | 95 ++++++++++--------- .../cryptofs/CryptoPathMapper.java | 30 +++--- .../org/cryptomator/cryptofs/Symlinks.java | 4 +- .../attr/AbstractCryptoFileAttributeView.java | 4 +- .../cryptofs/attr/AttributeProvider.java | 4 +- .../cryptofs/fh/OpenCryptoFile.java | 1 - .../cryptofs/CryptoFileSystemImplTest.java | 66 ++++++++----- .../cryptofs/CryptoPathMapperTest.java | 2 +- .../cryptomator/cryptofs/SymlinksTest.java | 22 ++--- .../cryptofs/attr/AttributeProviderTest.java | 29 +++--- .../attr/CryptoDosFileAttributeViewTest.java | 21 ++-- .../CryptoFileOwnerAttributeViewTest.java | 18 ++-- .../CryptoPosixFileAttributeViewTest.java | 20 ++-- 14 files changed, 236 insertions(+), 136 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java new file mode 100644 index 00000000..a9590693 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -0,0 +1,56 @@ +package org.cryptomator.cryptofs; + +import java.nio.file.Path; +import java.util.Objects; + +public class CiphertextFilePath { + + private final Path path; + private final boolean isShortened; + + // TODO: add deflatedName instead of caching it inside the longFileNameProvider + CiphertextFilePath(Path path, boolean isShortened) { + this.path = Objects.requireNonNull(path); + this.isShortened = isShortened; + } + + public Path getRawPath() { + return path; + } + + public boolean isShortened() { + return isShortened; + } + + public Path getFilePath() { + return isShortened ? path.resolve(Constants.CONTENTS_FILE_NAME) : path; + } + + public Path getDirFilePath() { + return path.resolve(Constants.DIR_FILE_NAME); + } + + public Path getSymlinkFilePath() { + return path.resolve(Constants.SYMLINK_FILE_NAME); + } + + @Override + public int hashCode() { + return Objects.hash(path, isShortened); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CiphertextFilePath) { + CiphertextFilePath other = (CiphertextFilePath) obj; + return this.path.equals(other.path) && this.isShortened == other.isShortened; + } else { + return false; + } + } + + @Override + public String toString() { + return path.toString(); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index f0a3b3f2..c5a85f06 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -296,11 +296,11 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws throw new NoSuchFileException(cleartextParentDir.toString()); } cryptoPathMapper.assertNonExisting(cleartextDir); - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); - Path ciphertextDirFile = ciphertextPath.resolve(Constants.DIR_FILE_NAME); + CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); + Path ciphertextDirFile = ciphertextPath.getDirFilePath(); CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); // atomically check for FileAlreadyExists and create otherwise: - Files.createDirectory(ciphertextPath); + Files.createDirectory(ciphertextPath.getRawPath()); try (FileChannel channel = FileChannel.open(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), attrs)) { channel.write(ByteBuffer.wrap(ciphertextDir.dirId.getBytes(UTF_8))); } @@ -352,13 +352,13 @@ FileChannel newFileChannel(CryptoPath cleartextPath, Set o } private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath); - if (options.createNew() && openCryptoFiles.get(ciphertextPath).isPresent()) { + CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath); + Path ciphertextFilePath = ciphertextPath.getFilePath(); + if (options.createNew() && openCryptoFiles.get(ciphertextFilePath).isPresent()) { throw new FileAlreadyExistsException(cleartextFilePath.toString()); } else { - // might also throw FileAlreadyExists: - FileChannel ch = openCryptoFiles.getOrCreate(ciphertextPath).newFileChannel(options); - longFileNameProvider.getCached(ciphertextPath).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options); // might throw FileAlreadyExists + longFileNameProvider.getCached(ciphertextFilePath).ifPresent(LongFileNameProvider.DeflatedFileName::persist); return ch; } } @@ -366,23 +366,23 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp void delete(CryptoPath cleartextPath) throws IOException { readonlyFlag.assertWritable(); CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath); - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); + CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); switch (ciphertextFileType) { case DIRECTORY: deleteDirectory(cleartextPath, ciphertextPath); return; default: - Files.walkFileTree(ciphertextPath, DeletingFileVisitor.INSTANCE); + Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); return; } } - private void deleteDirectory(CryptoPath cleartextPath, Path ciphertextPath) throws IOException { + private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException { Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path; - Path ciphertextDirFile = ciphertextPath.resolve(Constants.DIR_FILE_NAME); + Path ciphertextDirFile = ciphertextPath.getDirFilePath(); try { ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath); - Files.walkFileTree(ciphertextPath, DeletingFileVisitor.INSTANCE); + Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); cryptoPathMapper.invalidatePathMapping(cleartextPath); dirIdProvider.delete(ciphertextDirFile); } catch (NoSuchFileException e) { @@ -420,11 +420,11 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) { - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + CiphertextFilePath ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new); - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile); - Files.copy(ciphertextSourceFile, ciphertextTargetFile, resolvedOptions); + Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile.getRawPath()); + Files.copy(ciphertextSourceFile.getRawPath(), ciphertextTargetFile.getRawPath(), resolvedOptions); deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); } else { CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource); @@ -435,19 +435,22 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, } private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile); - Files.copy(ciphertextSourceFile, ciphertextTargetFile, options); + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + if (ciphertextTarget.isShortened()) { + Files.createDirectories(ciphertextTarget.getRawPath()); + } + Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTarget.getRawPath()); + Files.copy(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); 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); - if (Files.notExists(ciphertextTargetDirFile)) { + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + if (Files.notExists(ciphertextTarget.getRawPath())) { // create new: - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetDirFile); + Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTarget.getRawPath()); createDirectory(cleartextTarget); deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); } else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { @@ -459,7 +462,7 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge } } } else { - throw new FileAlreadyExistsException(cleartextTarget.toString(), null, "Ciphertext file already exists: " + ciphertextTargetDirFile); + throw new FileAlreadyExistsException(cleartextTarget.toString(), null, "Ciphertext file already exists: " + ciphertextTarget); } if (ArrayUtils.contains(options, StandardCopyOption.COPY_ATTRIBUTES)) { Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path; @@ -524,11 +527,11 @@ void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // according to Files.move() JavaDoc: // "the symbolic link itself, not the target of the link, is moved" - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) { - Files.move(ciphertextSourceFile, ciphertextTargetFile, options); - longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { + Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); twoPhaseMove.commit(); } } @@ -536,11 +539,17 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // While moving a file, it is possible to keep the channels open. In order to make this work // we need to re-map the OpenCryptoFile entry. - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSourceFile, ciphertextTargetFile)) { - Files.move(ciphertextSourceFile, ciphertextTargetFile, options); - longFileNameProvider.getCached(ciphertextTargetFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { + if (ciphertextTarget.isShortened()) { + Files.createDirectory(ciphertextTarget.getRawPath()); + } + Files.move(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); + if (ciphertextSource.isShortened()) { + Files.walkFileTree(ciphertextSource.getRawPath(), DeletingFileVisitor.INSTANCE); + } + longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); twoPhaseMove.commit(); } } @@ -548,12 +557,12 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // Since we only rename the directory file, all ciphertext paths of subresources stay the same. // Hence there is no need to re-map OpenCryptoFile entries. - Path ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - Path ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // try to move, don't replace: - Files.move(ciphertextSource, ciphertextTarget, options); - longFileNameProvider.getCached(ciphertextTarget).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); } else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { // replace atomically (impossible): assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); @@ -562,7 +571,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge // move and replace (if dir is empty): assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); assert !ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE); - if (Files.exists(ciphertextTarget)) { + if (Files.exists(ciphertextTarget.getRawPath())) { Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; try (DirectoryStream ds = Files.newDirectoryStream(ciphertextTargetDir)) { if (ds.iterator().hasNext()) { @@ -571,10 +580,10 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge } Files.delete(ciphertextTargetDir); } - Files.move(ciphertextSource, ciphertextTarget, options); - longFileNameProvider.getCached(ciphertextTarget).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); } - dirIdProvider.move(ciphertextSource.resolve(Constants.DIR_FILE_NAME), ciphertextTarget.resolve(Constants.DIR_FILE_NAME)); + dirIdProvider.move(ciphertextSource.getDirFilePath(), ciphertextTarget.getDirFilePath()); cryptoPathMapper.invalidatePathMapping(cleartextSource); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 4e56bbea..203601ad 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -67,8 +67,8 @@ public class CryptoPathMapper { */ public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException { try { - Path ciphertextPath = getCiphertextFilePath(cleartextPath); - BasicFileAttributes attr = Files.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + CiphertextFilePath ciphertextPath = getCiphertextFilePath(cleartextPath); + BasicFileAttributes attr = Files.readAttributes(ciphertextPath.getRawPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); if (attr != null) { throw new FileAlreadyExistsException(cleartextPath.toString()); } @@ -88,27 +88,24 @@ public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws if (parentPath == null) { return CiphertextFileType.DIRECTORY; // ROOT } else { - Path ciphertextPath = getCiphertextFilePath(cleartextPath); - BasicFileAttributes attr = Files.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + CiphertextFilePath ciphertextPath = getCiphertextFilePath(cleartextPath); + BasicFileAttributes attr = Files.readAttributes(ciphertextPath.getRawPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); if (attr.isRegularFile()) { return CiphertextFileType.FILE; } else if (attr.isDirectory()) { - Path symlinkFilePath = ciphertextPath.resolve(Constants.SYMLINK_FILE_NAME); - Path dirFilePath = ciphertextPath.resolve(Constants.DIR_FILE_NAME); - Path contentsFilePath = ciphertextPath.resolve(Constants.CONTENTS_FILE_NAME); - if (Files.exists(dirFilePath, LinkOption.NOFOLLOW_LINKS)) { + if (Files.exists(ciphertextPath.getDirFilePath(), LinkOption.NOFOLLOW_LINKS)) { return CiphertextFileType.DIRECTORY; - } else if (Files.exists(symlinkFilePath, LinkOption.NOFOLLOW_LINKS)) { + } else if (Files.exists(ciphertextPath.getSymlinkFilePath(), LinkOption.NOFOLLOW_LINKS)) { return CiphertextFileType.SYMLINK; - } else if (Files.exists(contentsFilePath, LinkOption.NOFOLLOW_LINKS)) { + } else if (Files.exists(ciphertextPath.getFilePath(), LinkOption.NOFOLLOW_LINKS)) { return CiphertextFileType.FILE; } } - throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath); + throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath.getRawPath()); } } - public Path getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { + public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { CryptoPath parentPath = cleartextPath.getParent(); if (parentPath == null) { throw new IllegalArgumentException("Invalid file path (must have a parent): " + cleartextPath); @@ -116,11 +113,12 @@ public Path getCiphertextFilePath(CryptoPath cleartextPath) throws IOException { CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parent.dirId, cleartextName)); - Path canonicalCiphertextPath = parent.path.resolve(ciphertextName); + Path unshortenedName = parent.path.resolve(ciphertextName); if (ciphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH) { - return longFileNameProvider.deflate(canonicalCiphertextPath); + Path shortenedName = longFileNameProvider.deflate(unshortenedName); + return new CiphertextFilePath(shortenedName, true); } else { - return canonicalCiphertextPath; + return new CiphertextFilePath(unshortenedName, false); } } @@ -140,7 +138,7 @@ public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOE } else { try { return ciphertextDirectories.get(cleartextPath, () -> { - Path dirIdFile = getCiphertextFilePath(cleartextPath).resolve(Constants.DIR_FILE_NAME); + Path dirIdFile = getCiphertextFilePath(cleartextPath).getDirFilePath(); return resolveDirectory(dirIdFile); }); } catch (ExecutionException e) { diff --git a/src/main/java/org/cryptomator/cryptofs/Symlinks.java b/src/main/java/org/cryptomator/cryptofs/Symlinks.java index 92971377..0a7c141a 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -42,7 +42,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib if (target.toString().length() > Constants.MAX_SYMLINK_LENGTH) { throw new IOException("path length limit exceeded."); } - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).resolve(Constants.SYMLINK_FILE_NAME); + Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).getSymlinkFilePath(); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag); ByteBuffer content = UTF_8.encode(target.toString()); Files.createDirectory(ciphertextSymlinkFile.getParent()); @@ -51,7 +51,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib } public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException { - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).resolve(Constants.SYMLINK_FILE_NAME); + Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).getSymlinkFilePath(); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.READ), readonlyFlag); assertIsSymlink(cleartextPath, ciphertextSymlinkFile); try { diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 130d53e8..0fd693bc 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -53,7 +53,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException { switch (type) { case SYMLINK: if (ArrayUtils.contains(linkOptions, LinkOption.NOFOLLOW_LINKS)) { - return pathMapper.getCiphertextFilePath(path); + return pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); } else { CryptoPath resolved = symlinks.resolveRecursively(path); return getCiphertextPath(resolved); @@ -61,7 +61,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException { case DIRECTORY: return pathMapper.getCiphertextDir(path).path; default: - return pathMapper.getCiphertextFilePath(path); + return pathMapper.getCiphertextFilePath(path).getFilePath(); } } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java index 0d5f14e8..f67d8e24 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java @@ -66,7 +66,7 @@ public A readAttributes(CryptoPath cleartextPath switch (ciphertextFileType) { case SYMLINK: { if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) { - Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath); + Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath).getSymlinkFilePath(); return readAttributes(ciphertextFileType, ciphertextPath, type); } else { CryptoPath resolved = symlinks.resolveRecursively(cleartextPath); @@ -78,7 +78,7 @@ public A readAttributes(CryptoPath cleartextPath return readAttributes(ciphertextFileType, ciphertextPath, type); } case FILE: { - Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath); + Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath).getFilePath(); return readAttributes(ciphertextFileType, ciphertextPath, type); } default: diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java index 1217d628..d9352fa6 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs.fh; -import com.google.common.base.Preconditions; import org.cryptomator.cryptofs.EffectiveOpenOptions; import org.cryptomator.cryptofs.ch.ChannelComponent; import org.cryptomator.cryptofs.ch.CleartextFileChannel; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index fb3bcc22..4aa5b51b 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -31,7 +31,6 @@ import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; -import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -340,9 +339,10 @@ public void testNewWatchServiceThrowsUnsupportedOperationException() throws IOEx public class Delete { private final CryptoPath cleartextPath = mock(CryptoPath.class, "cleartext"); - private final Path ciphertextPath = mock(Path.class, "d/00/00/path.c9r"); + 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 ciphertextDirPath = mock(Path.class, "d/FF/FF/"); + private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); private final FileSystem physicalFs = mock(FileSystem.class); private final FileSystemProvider physicalFsProv = mock(FileSystemProvider.class); private final BasicFileAttributes ciphertextPathAttr = mock(BasicFileAttributes.class); @@ -351,13 +351,15 @@ public class Delete { @BeforeEach public void setup() throws IOException { when(physicalFs.provider()).thenReturn(physicalFsProv); - when(ciphertextPath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextRawPath.getFileSystem()).thenReturn(physicalFs); when(ciphertextDirPath.getFileSystem()).thenReturn(physicalFs); when(ciphertextDirFilePath.getFileSystem()).thenReturn(physicalFs); - when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFilePath); + when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFilePath); when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFilePath); when(cryptoPathMapper.getCiphertextDir(cleartextPath)).thenReturn(new CiphertextDirectory("foo", ciphertextDirPath)); - when(physicalFsProv.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); + when(physicalFsProv.readAttributes(ciphertextRawPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); when(physicalFsProv.readAttributes(ciphertextDirFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextDirFilePathAttr); } @@ -365,20 +367,20 @@ public void setup() throws IOException { @Test public void testDeleteExistingFile() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(true); + when(physicalFsProv.deleteIfExists(ciphertextRawPath)).thenReturn(true); inTest.delete(cleartextPath); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).deleteIfExists(ciphertextPath); + verify(physicalFsProv).deleteIfExists(ciphertextRawPath); } @Test public void testDeleteExistingDirectory() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(false); + when(physicalFsProv.deleteIfExists(ciphertextRawPath)).thenReturn(false); when(ciphertextPathAttr.isDirectory()).thenReturn(true); - when(physicalFsProv.newDirectoryStream(Mockito.eq(ciphertextPath), Mockito.any())).thenReturn(new DirectoryStream() { + when(physicalFsProv.newDirectoryStream(Mockito.eq(ciphertextRawPath), Mockito.any())).thenReturn(new DirectoryStream() { @Override public Iterator iterator() { return Arrays.asList(ciphertextDirFilePath).iterator(); @@ -394,7 +396,7 @@ public void close() { verify(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath); verify(readonlyFlag).assertWritable(); verify(physicalFsProv).deleteIfExists(ciphertextDirFilePath); - verify(physicalFsProv).deleteIfExists(ciphertextPath); + verify(physicalFsProv).deleteIfExists(ciphertextRawPath); verify(dirIdProvider).delete(ciphertextDirFilePath); verify(cryptoPathMapper).invalidatePathMapping(cleartextPath); } @@ -411,7 +413,7 @@ public void testDeleteNonExistingFileOrDir() throws IOException { @Test public void testDeleteNonEmptyDir() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - when(physicalFsProv.deleteIfExists(ciphertextPath)).thenReturn(false); + when(physicalFsProv.deleteIfExists(ciphertextRawPath)).thenReturn(false); Mockito.doThrow(new DirectoryNotEmptyException("ciphertextDir")).when(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { @@ -428,6 +430,8 @@ public class CopyAndMove { private final CryptoPath sourceLinkTarget = mock(CryptoPath.class, "sourceLinkTarget"); private final CryptoPath cleartextDestination = mock(CryptoPath.class, "cleartextDestination"); private final CryptoPath destinationLinkTarget = mock(CryptoPath.class, "destinationLinkTarget"); + private final CiphertextFilePath ciphertextSource = mock(CiphertextFilePath.class, "ciphertextSource"); + private final CiphertextFilePath ciphertextDestination = mock(CiphertextFilePath.class, "ciphertextDestination"); private final Path ciphertextSourceFile = mock(Path.class, "d/00/00/source.c9r"); private final Path ciphertextSourceDirFile = mock(Path.class, "d/00/00/source.c9r/dir.c9r"); private final Path ciphertextSourceDir = mock(Path.class, "d/00/SOURCE/"); @@ -439,8 +443,14 @@ public class CopyAndMove { @BeforeEach public void setup() throws IOException { - when(ciphertextSourceFile.resolve("dir.c9r")).thenReturn(ciphertextSourceDirFile); - when(ciphertextDestinationFile.resolve("dir.c9r")).thenReturn(ciphertextDestinationDirFile); + when(ciphertextSource.getRawPath()).thenReturn(ciphertextSourceFile); + when(ciphertextSource.getFilePath()).thenReturn(ciphertextSourceFile); + when(ciphertextSource.getSymlinkFilePath()).thenReturn(ciphertextSourceFile); + when(ciphertextSource.getDirFilePath()).thenReturn(ciphertextSourceDirFile); + when(ciphertextDestination.getRawPath()).thenReturn(ciphertextDestinationFile); + when(ciphertextDestination.getFilePath()).thenReturn(ciphertextDestinationFile); + when(ciphertextDestination.getSymlinkFilePath()).thenReturn(ciphertextDestinationFile); + when(ciphertextDestination.getDirFilePath()).thenReturn(ciphertextDestinationDirFile); when(ciphertextSourceFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDir.getFileSystem()).thenReturn(physicalFs); @@ -448,16 +458,16 @@ public void setup() throws IOException { when(ciphertextDestinationDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextDestinationDir.getFileSystem()).thenReturn(physicalFs); when(physicalFs.provider()).thenReturn(physicalFsProv); - when(cryptoPathMapper.getCiphertextFilePath(cleartextSource)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination)).thenReturn(ciphertextDestinationFile); + when(cryptoPathMapper.getCiphertextFilePath(cleartextSource)).thenReturn(ciphertextSource); + when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination)).thenReturn(ciphertextDestination); when(cryptoPathMapper.getCiphertextDir(cleartextSource)).thenReturn(new CiphertextDirectory("foo", ciphertextSourceDir)); when(cryptoPathMapper.getCiphertextDir(cleartextDestination)).thenReturn(new CiphertextDirectory("bar", ciphertextDestinationDir)); when(symlinks.resolveRecursively(cleartextSource)).thenReturn(sourceLinkTarget); when(symlinks.resolveRecursively(cleartextDestination)).thenReturn(destinationLinkTarget); when(cryptoPathMapper.getCiphertextFileType(sourceLinkTarget)).thenReturn(CiphertextFileType.FILE); when(cryptoPathMapper.getCiphertextFileType(destinationLinkTarget)).thenReturn(CiphertextFileType.FILE); - when(cryptoPathMapper.getCiphertextFilePath(sourceLinkTarget)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget)).thenReturn(ciphertextDestinationFile); + when(cryptoPathMapper.getCiphertextFilePath(sourceLinkTarget)).thenReturn(ciphertextSource); + when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget)).thenReturn(ciphertextDestination); } @Nested @@ -904,19 +914,22 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio CryptoPath path = mock(CryptoPath.class, "path"); CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); - Path ciphertextPath = mock(Path.class, "d/00/00/path.c9r"); + Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r"); Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); Path ciphertextDirPath = mock(Path.class, "d/FF/FF/"); + CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); when(path.getParent()).thenReturn(parent); - when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); + when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath)); when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class); + when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); - when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem); when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel); @@ -932,19 +945,22 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro CryptoPath path = mock(CryptoPath.class, "path"); CryptoPath parent = mock(CryptoPath.class, "parent"); Path ciphertextParent = mock(Path.class, "ciphertextParent"); - Path ciphertextPath = mock(Path.class, "d/00/00/path.c9r"); + Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r"); Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r"); Path ciphertextDirPath = mock(Path.class, "d/FF/FF/"); + CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); String dirId = "DirId1234ABC"; FileChannelMock channel = new FileChannelMock(100); when(path.getParent()).thenReturn(parent); - when(ciphertextPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); + when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath)); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextDirPath)); when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class); + when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextDirFile); when(ciphertextParent.getFileSystem()).thenReturn(fileSystem); - when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem); when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem); when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel); @@ -1237,9 +1253,11 @@ public void setAttributeOnFile() throws IOException { CryptoPath path = mock(CryptoPath.class); Path ciphertextDirPath = mock(Path.class); Path ciphertextFilePath = mock(Path.class); + CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); when(cryptoPathMapper.getCiphertextFileType(path)).thenReturn(CiphertextFileType.FILE); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory("foo", ciphertextDirPath)); - when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextFilePath); + when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath); doThrow(new NoSuchFileException("")).when(provider).checkAccess(ciphertextDirPath); inTest.setAttribute(path, name, value); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index af8f63d2..7424b3a3 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -162,7 +162,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException { Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("baz"), Mockito.any())).thenReturn("zab"); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider); - Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")); + Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")).getRawPath(); Assertions.assertEquals(d0002zab, path); } diff --git a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java index 2a6c74df..2c95be75 100644 --- a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java +++ b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java @@ -37,20 +37,23 @@ public void setup() throws IOException { } private Path mockExistingSymlink(CryptoPath cleartextPath) throws IOException { - Path ciphertextPath = Mockito.mock(Path.class); + Path ciphertextRawPath = Mockito.mock(Path.class); Path symlinkFilePath = Mockito.mock(Path.class); BasicFileAttributes ciphertextPathAttr = Mockito.mock(BasicFileAttributes.class); BasicFileAttributes symlinkFilePathAttr = Mockito.mock(BasicFileAttributes.class); - Mockito.when(ciphertextPath.resolve("symlink.c9r")).thenReturn(symlinkFilePath); - Mockito.when(symlinkFilePath.getParent()).thenReturn(ciphertextPath); - Mockito.when(ciphertextPath.getFileSystem()).thenReturn(underlyingFs); + CiphertextFilePath ciphertextPath = Mockito.mock(CiphertextFilePath.class); + Mockito.when(ciphertextRawPath.resolve("symlink.c9r")).thenReturn(symlinkFilePath); + Mockito.when(symlinkFilePath.getParent()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextRawPath.getFileSystem()).thenReturn(underlyingFs); Mockito.when(symlinkFilePath.getFileSystem()).thenReturn(underlyingFs); - Mockito.when(underlyingFsProvider.readAttributes(ciphertextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); + Mockito.when(underlyingFsProvider.readAttributes(ciphertextRawPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(ciphertextPathAttr); Mockito.when(underlyingFsProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(symlinkFilePathAttr); Mockito.when(ciphertextPathAttr.isDirectory()).thenReturn(true); Mockito.when(symlinkFilePathAttr.isRegularFile()).thenReturn(true); Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); - return ciphertextPath; + Mockito.when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextPath.getSymlinkFilePath()).thenReturn(symlinkFilePath); + return ciphertextRawPath; } @Test @@ -80,7 +83,6 @@ public void testReadSymbolicLink() throws IOException { CryptoPath resolvedTargetPath = Mockito.mock(CryptoPath.class, "resolvedTargetPath"); Path ciphertextPath = mockExistingSymlink(cleartextPath); Path symlinkFilePath = ciphertextPath.resolve("symlink.c9r"); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(symlinkFilePath), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode(targetPath)); Mockito.when(cleartextFs.getPath(targetPath)).thenReturn(resolvedTargetPath); @@ -116,8 +118,6 @@ public void testResolveRecursively() throws IOException { Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath3)).thenReturn(CiphertextFileType.FILE); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2)).thenReturn(ciphertextPath2); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); Mockito.when(cleartextFs.getPath("file2")).thenReturn(cleartextPath2); @@ -139,7 +139,6 @@ public void testResolveRecursivelyWithNonExistingTarget() throws IOException { Mockito.when(cleartextPath2.getFileSystem()).thenReturn(cleartextFs); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenThrow(new NoSuchFileException("cleartextPath2")); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); Mockito.when(cleartextFs.getPath("file2")).thenReturn(cleartextPath2); @@ -166,9 +165,6 @@ public void testResolveRecursivelyWithLoop() throws IOException { Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath1)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath2)).thenReturn(CiphertextFileType.SYMLINK); Mockito.when(cryptoPathMapper.getCiphertextFileType(cleartextPath3)).thenReturn(CiphertextFileType.SYMLINK); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath1)).thenReturn(ciphertextPath1); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath2)).thenReturn(ciphertextPath2); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(cleartextPath3)).thenReturn(ciphertextPath3); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath1), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file2")); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath2), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file3")); Mockito.when(openCryptoFiles.readCiphertextFile(Mockito.eq(ciphertextSymlinkPath3), Mockito.any(), Mockito.anyInt())).thenReturn(StandardCharsets.UTF_8.encode("file1")); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java index 6af1fd92..4fab54a6 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; +import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.CiphertextFileType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoPath; @@ -40,7 +41,8 @@ public class AttributeProviderTest { private OpenCryptoFiles openCryptoFiles; private CryptoFileSystemProperties fileSystemProperties; private CryptoPath cleartextPath; - private Path ciphertextFilePath; + private CiphertextFilePath ciphertextPath; + private Path ciphertextRawPath; private Symlinks symlinks; @BeforeEach @@ -50,21 +52,26 @@ public void setup() throws IOException { openCryptoFiles = Mockito.mock(OpenCryptoFiles.class); fileSystemProperties = Mockito.mock(CryptoFileSystemProperties.class); cleartextPath = Mockito.mock(CryptoPath.class, "cleartextPath"); - ciphertextFilePath = Mockito.mock(Path.class, "ciphertextPath"); + ciphertextRawPath = Mockito.mock(Path.class, "ciphertextPath"); + ciphertextPath = Mockito.mock(CiphertextFilePath.class); symlinks = Mockito.mock(Symlinks.class); FileSystem fs = Mockito.mock(FileSystem.class); - Mockito.when(ciphertextFilePath.getFileSystem()).thenReturn(fs); + Mockito.when(ciphertextRawPath.getFileSystem()).thenReturn(fs); FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); Mockito.when(fs.provider()).thenReturn(provider); BasicFileAttributes basicAttr = Mockito.mock(BasicFileAttributes.class); PosixFileAttributes posixAttr = Mockito.mock(PosixFileAttributes.class); DosFileAttributes dosAttr = Mockito.mock(DosFileAttributes.class); - Mockito.when(provider.readAttributes(Mockito.same(ciphertextFilePath), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(basicAttr); - Mockito.when(provider.readAttributes(Mockito.same(ciphertextFilePath), Mockito.same(PosixFileAttributes.class), Mockito.any())).thenReturn(posixAttr); - Mockito.when(provider.readAttributes(Mockito.same(ciphertextFilePath), Mockito.same(DosFileAttributes.class), Mockito.any())).thenReturn(dosAttr); + Mockito.when(provider.readAttributes(Mockito.same(ciphertextRawPath), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(basicAttr); + Mockito.when(provider.readAttributes(Mockito.same(ciphertextRawPath), Mockito.same(PosixFileAttributes.class), Mockito.any())).thenReturn(posixAttr); + Mockito.when(provider.readAttributes(Mockito.same(ciphertextRawPath), Mockito.same(DosFileAttributes.class), Mockito.any())).thenReturn(dosAttr); Mockito.when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + Mockito.when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextPath.getDirFilePath()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextPath.getSymlinkFilePath()).thenReturn(ciphertextRawPath); // needed for cleartxt file size calculation FileHeaderCryptor fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); @@ -87,7 +94,7 @@ public class Files { @BeforeEach public void setup() throws IOException { Mockito.when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); } @Test @@ -131,7 +138,7 @@ public class Directories { @BeforeEach public void setup() throws IOException { Mockito.when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - Mockito.when(pathMapper.getCiphertextDir(cleartextPath)).thenReturn(new CiphertextDirectory("foo", ciphertextFilePath)); + Mockito.when(pathMapper.getCiphertextDir(cleartextPath)).thenReturn(new CiphertextDirectory("foo", ciphertextRawPath)); } @Test @@ -154,7 +161,7 @@ public void setup() throws IOException { @Test public void testReadBasicAttributesNoFollow() throws IOException { - Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); AttributeProvider prov = new AttributeProvider(cryptor, pathMapper, openCryptoFiles, fileSystemProperties, symlinks); BasicFileAttributes attr = prov.readAttributes(cleartextPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); @@ -167,7 +174,7 @@ public void testReadBasicAttributesOfTarget() throws IOException { CryptoPath targetPath = Mockito.mock(CryptoPath.class, "targetPath"); Mockito.when(symlinks.resolveRecursively(cleartextPath)).thenReturn(targetPath); Mockito.when(pathMapper.getCiphertextFileType(targetPath)).thenReturn(CiphertextFileType.FILE); - Mockito.when(pathMapper.getCiphertextFilePath(targetPath)).thenReturn(ciphertextFilePath); + Mockito.when(pathMapper.getCiphertextFilePath(targetPath)).thenReturn(ciphertextPath); AttributeProvider prov = new AttributeProvider(cryptor, pathMapper, openCryptoFiles, fileSystemProperties, symlinks); BasicFileAttributes attr = prov.readAttributes(cleartextPath, BasicFileAttributes.class); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java index f2486a58..2d53ad8f 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.attr; +import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; @@ -27,9 +28,10 @@ public class CryptoDosFileAttributeViewTest { - private Path linkCiphertextPath = mock(Path.class); - - private Path ciphertextPath = mock(Path.class); + private CiphertextFilePath linkCiphertextPath = mock(CiphertextFilePath.class); + private CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); + private Path linkCiphertextRawPath = mock(Path.class); + private Path ciphertextRawPath = mock(Path.class); private FileSystem fileSystem = mock(FileSystem.class); private FileSystemProvider provider = mock(FileSystemProvider.class); private DosFileAttributeView delegate = mock(DosFileAttributeView.class); @@ -47,13 +49,13 @@ public class CryptoDosFileAttributeViewTest { @BeforeEach public void setup() throws IOException { - when(linkCiphertextPath.getFileSystem()).thenReturn(fileSystem); + when(linkCiphertextRawPath.getFileSystem()).thenReturn(fileSystem); - when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); when(fileSystem.provider()).thenReturn(provider); - when(provider.getFileAttributeView(ciphertextPath, DosFileAttributeView.class)).thenReturn(delegate); - when(provider.getFileAttributeView(ciphertextPath, BasicFileAttributeView.class)).thenReturn(delegate); - when(provider.getFileAttributeView(linkCiphertextPath, DosFileAttributeView.class)).thenReturn(linkDelegate); + when(provider.getFileAttributeView(ciphertextRawPath, DosFileAttributeView.class)).thenReturn(delegate); + when(provider.getFileAttributeView(ciphertextRawPath, BasicFileAttributeView.class)).thenReturn(delegate); + when(provider.getFileAttributeView(linkCiphertextRawPath, DosFileAttributeView.class)).thenReturn(linkDelegate); when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); @@ -61,6 +63,9 @@ public void setup() throws IOException { when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + inTest = new CryptoDosFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, fileAttributeProvider, readonlyFlag); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java index 1b993d9e..87203417 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.attr; +import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; @@ -25,8 +26,10 @@ public class CryptoFileOwnerAttributeViewTest { - private Path linkCiphertextPath = mock(Path.class); - private Path ciphertextPath = mock(Path.class); + private CiphertextFilePath linkCiphertextPath = mock(CiphertextFilePath.class); + private CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); + private Path linkCiphertextRawPath = mock(Path.class); + private Path ciphertextRawPath = mock(Path.class); private FileSystem fileSystem = mock(FileSystem.class); private FileSystemProvider provider = mock(FileSystemProvider.class); private FileOwnerAttributeView delegate = mock(FileOwnerAttributeView.class); @@ -43,11 +46,11 @@ public class CryptoFileOwnerAttributeViewTest { @BeforeEach public void setup() throws IOException { - when(linkCiphertextPath.getFileSystem()).thenReturn(fileSystem); - when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); + when(linkCiphertextRawPath.getFileSystem()).thenReturn(fileSystem); + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); when(fileSystem.provider()).thenReturn(provider); - when(provider.getFileAttributeView(ciphertextPath, FileOwnerAttributeView.class)).thenReturn(delegate); - when(provider.getFileAttributeView(linkCiphertextPath, FileOwnerAttributeView.class)).thenReturn(linkDelegate); + when(provider.getFileAttributeView(ciphertextRawPath, FileOwnerAttributeView.class)).thenReturn(delegate); + when(provider.getFileAttributeView(linkCiphertextRawPath, FileOwnerAttributeView.class)).thenReturn(linkDelegate); when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); @@ -55,6 +58,9 @@ public void setup() throws IOException { when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java index 4a5026a5..6806f53a 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.attr; +import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; @@ -34,8 +35,10 @@ public class CryptoPosixFileAttributeViewTest { - private Path linkCiphertextPath = mock(Path.class); - private Path ciphertextPath = mock(Path.class); + private CiphertextFilePath linkCiphertextPath = mock(CiphertextFilePath.class); + private CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); + private Path linkCiphertextRawPath = mock(Path.class); + private Path ciphertextRawPath = mock(Path.class); private FileSystem fileSystem = mock(FileSystem.class); private FileSystemProvider provider = mock(FileSystemProvider.class); private PosixFileAttributeView delegate = mock(PosixFileAttributeView.class); @@ -53,12 +56,12 @@ public class CryptoPosixFileAttributeViewTest { @BeforeEach public void setUp() throws IOException { - when(linkCiphertextPath.getFileSystem()).thenReturn(fileSystem); - when(ciphertextPath.getFileSystem()).thenReturn(fileSystem); + when(linkCiphertextRawPath.getFileSystem()).thenReturn(fileSystem); + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); when(fileSystem.provider()).thenReturn(provider); - when(provider.getFileAttributeView(ciphertextPath, PosixFileAttributeView.class)).thenReturn(delegate); - when(provider.getFileAttributeView(ciphertextPath, BasicFileAttributeView.class)).thenReturn(delegate); - when(provider.getFileAttributeView(linkCiphertextPath, PosixFileAttributeView.class)).thenReturn(linkDelegate); + when(provider.getFileAttributeView(ciphertextRawPath, PosixFileAttributeView.class)).thenReturn(delegate); + when(provider.getFileAttributeView(ciphertextRawPath, BasicFileAttributeView.class)).thenReturn(delegate); + when(provider.getFileAttributeView(linkCiphertextRawPath, PosixFileAttributeView.class)).thenReturn(linkDelegate); when(symlinks.resolveRecursively(link)).thenReturn(cleartextPath); when(pathMapper.getCiphertextFileType(link)).thenReturn(CiphertextFileType.SYMLINK); @@ -66,6 +69,9 @@ public void setUp() throws IOException { when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + inTest = new CryptoPosixFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, fileAttributeProvider, readonlyFlag); } From 05ce540953d7c4000e8f8dea95f39ccc0b615033 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 3 Sep 2019 15:57:18 +0200 Subject: [PATCH 26/62] Fixed creation of files with long names --- .../java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index c5a85f06..e4f20412 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -357,6 +357,11 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp if (options.createNew() && openCryptoFiles.get(ciphertextFilePath).isPresent()) { throw new FileAlreadyExistsException(cleartextFilePath.toString()); } else { + if (ciphertextPath.isShortened() && options.createNew()) { + Files.createDirectory(ciphertextPath.getRawPath()); // might throw FileAlreadyExists + } else if (ciphertextPath.isShortened()) { + Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists + } FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options); // might throw FileAlreadyExists longFileNameProvider.getCached(ciphertextFilePath).ifPresent(LongFileNameProvider.DeflatedFileName::persist); return ch; From 91c7b416d1330232480f41bb2975bb625c7a411e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Sep 2019 11:56:25 +0200 Subject: [PATCH 27/62] Deflated file names are strongly referenced as long as they aren't persisted instead of relying on cached values. See discussion with @buddydvd on cbbd510. --- .../cryptofs/CiphertextFilePath.java | 20 +++++++------ .../cryptofs/ConflictResolver.java | 27 +++++++++--------- .../cryptofs/CryptoFileSystemImpl.java | 21 ++++++-------- .../cryptofs/CryptoPathMapper.java | 15 ++++++---- .../cryptofs/LongFileNameProvider.java | 27 ++++-------------- .../org/cryptomator/cryptofs/Symlinks.java | 8 +++--- .../cryptofs/ConflictResolverTest.java | 28 +++++++++++-------- .../cryptofs/LongFileNameProviderTest.java | 14 ++++------ 8 files changed, 77 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java index a9590693..d54b73e4 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -2,16 +2,16 @@ import java.nio.file.Path; import java.util.Objects; +import java.util.Optional; public class CiphertextFilePath { private final Path path; - private final boolean isShortened; + private final Optional deflatedFileName; - // TODO: add deflatedName instead of caching it inside the longFileNameProvider - CiphertextFilePath(Path path, boolean isShortened) { + CiphertextFilePath(Path path, Optional deflatedFileName) { this.path = Objects.requireNonNull(path); - this.isShortened = isShortened; + this.deflatedFileName = Objects.requireNonNull(deflatedFileName); } public Path getRawPath() { @@ -19,11 +19,11 @@ public Path getRawPath() { } public boolean isShortened() { - return isShortened; + return deflatedFileName.isPresent(); } public Path getFilePath() { - return isShortened ? path.resolve(Constants.CONTENTS_FILE_NAME) : path; + return isShortened() ? path.resolve(Constants.CONTENTS_FILE_NAME) : path; } public Path getDirFilePath() { @@ -36,14 +36,14 @@ public Path getSymlinkFilePath() { @Override public int hashCode() { - return Objects.hash(path, isShortened); + return Objects.hash(path, deflatedFileName); } @Override public boolean equals(Object obj) { if (obj instanceof CiphertextFilePath) { CiphertextFilePath other = (CiphertextFilePath) obj; - return this.path.equals(other.path) && this.isShortened == other.isShortened; + return this.path.equals(other.path) && this.deflatedFileName.equals(other.deflatedFileName); } else { return false; } @@ -53,4 +53,8 @@ public boolean equals(Object obj) { public String toString() { return path.toString(); } + + public void persistLongFileName() { + deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + } } diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java index 980661d9..01d6f628 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java @@ -36,11 +36,13 @@ class ConflictResolver { private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes private final LongFileNameProvider longFileNameProvider; + private final CryptoPathMapper cryptoPathMapper; private final Cryptor cryptor; @Inject - public ConflictResolver(LongFileNameProvider longFileNameProvider, Cryptor cryptor) { + public ConflictResolver(LongFileNameProvider longFileNameProvider, CryptoPathMapper cryptoPathMapper, Cryptor cryptor) { this.longFileNameProvider = longFileNameProvider; + this.cryptoPathMapper = cryptoPathMapper; this.cryptor = cryptor; } @@ -120,21 +122,18 @@ private Path resolveConflict(Path conflictingPath, Path canonicalPath, String di */ private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId) throws IOException { assert Files.exists(canonicalPath); + Path ciphertextParentDir = canonicalPath.getParent(); try { String cleartext = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId.getBytes(StandardCharsets.UTF_8)); - Path alternativePath = canonicalPath; - for (int i = 1; Files.exists(alternativePath); i++) { - String alternativeCleartext = cleartext + " (Conflict " + i + ")"; - String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId.getBytes(StandardCharsets.UTF_8)); - String alternativeCiphertextFileName = alternativeCiphertext + CRYPTOMATOR_FILE_SUFFIX; - alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName); - if (alternativeCiphertextFileName.length() > SHORT_NAMES_MAX_LENGTH) { - alternativePath = longFileNameProvider.deflate(alternativePath); - } - } - LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); - Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); - longFileNameProvider.getCached(resolved).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + CiphertextFilePath alternativePath; + int i = 1; + do { + String alternativeCleartext = cleartext + " (Conflict " + i++ + ")"; + alternativePath = cryptoPathMapper.getCiphertextFilePath(ciphertextParentDir, dirId, alternativeCleartext); + } while(Files.exists(alternativePath.getRawPath())); + LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath.getRawPath()); + Path resolved = Files.move(conflictingPath, alternativePath.getRawPath(), StandardCopyOption.ATOMIC_MOVE); + alternativePath.persistLongFileName(); return resolved; } catch (AuthenticationFailedException e) { // not decryptable, no need to resolve any kind of conflict diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index e4f20412..88403c6b 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -307,7 +307,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.getCached(ciphertextDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextPath.persistLongFileName(); } catch (IOException e) { // make sure there is no orphan dir file: Files.delete(ciphertextDirFile); @@ -363,7 +363,7 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists } FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options); // might throw FileAlreadyExists - longFileNameProvider.getCached(ciphertextFilePath).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextPath.persistLongFileName(); return ch; } } @@ -428,9 +428,8 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CiphertextFilePath ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new); - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile.getRawPath()); Files.copy(ciphertextSourceFile.getRawPath(), ciphertextTargetFile.getRawPath(), resolvedOptions); - deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTargetFile.persistLongFileName(); } else { CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource); CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget); @@ -445,9 +444,8 @@ private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co if (ciphertextTarget.isShortened()) { Files.createDirectories(ciphertextTarget.getRawPath()); } - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTarget.getRawPath()); Files.copy(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); - deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTarget.persistLongFileName(); } private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { @@ -455,9 +453,8 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); if (Files.notExists(ciphertextTarget.getRawPath())) { // create new: - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTarget.getRawPath()); createDirectory(cleartextTarget); - deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTarget.persistLongFileName(); } else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // keep existing (if empty): Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; @@ -536,7 +533,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); - longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTarget.persistLongFileName(); twoPhaseMove.commit(); } } @@ -549,12 +546,12 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { if (ciphertextTarget.isShortened()) { Files.createDirectory(ciphertextTarget.getRawPath()); + ciphertextTarget.persistLongFileName(); } Files.move(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); if (ciphertextSource.isShortened()) { Files.walkFileTree(ciphertextSource.getRawPath(), DeletingFileVisitor.INSTANCE); } - longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); twoPhaseMove.commit(); } } @@ -567,7 +564,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // try to move, don't replace: Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); - longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTarget.persistLongFileName(); } else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { // replace atomically (impossible): assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); @@ -586,7 +583,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge Files.delete(ciphertextTargetDir); } Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); - longFileNameProvider.getCached(ciphertextTarget.getRawPath()).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextTarget.persistLongFileName(); } dirIdProvider.move(ciphertextSource.getDirFilePath(), ciphertextTarget.getDirFilePath()); cryptoPathMapper.invalidatePathMapping(cleartextSource); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 203601ad..a0643f14 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -27,6 +27,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ExecutionException; import static org.cryptomator.cryptofs.Constants.DATA_DIR_NAME; @@ -112,13 +113,17 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws } CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); - String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parent.dirId, cleartextName)); - Path unshortenedName = parent.path.resolve(ciphertextName); + return getCiphertextFilePath(parent.path, parent.dirId, cleartextName); + } + + public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) { + String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parentDirId, cleartextName)); + Path c9rPath = parentCiphertextDir.resolve(ciphertextName); if (ciphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH) { - Path shortenedName = longFileNameProvider.deflate(unshortenedName); - return new CiphertextFilePath(shortenedName, true); + LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath); + return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName)); } else { - return new CiphertextFilePath(unshortenedName, false); + return new CiphertextFilePath(c9rPath, Optional.empty()); } } diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index b3367ca9..cd104a62 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -26,7 +26,6 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Duration; -import java.util.Optional; import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; @@ -41,7 +40,7 @@ class LongFileNameProvider { private static final Duration MAX_CACHE_AGE = Duration.ofMinutes(1); private final ReadonlyFlag readonlyFlag; - private final LoadingCache longNames; + private final LoadingCache longNames; // Maps from c9s paths to inflated filenames @Inject public LongFileNameProvider(ReadonlyFlag readonlyFlag) { @@ -81,28 +80,14 @@ public String inflate(Path c9sPath) throws IOException { } } - public Path deflate(Path canonicalFileName) { - String longFileName = canonicalFileName.getFileName().toString(); + public DeflatedFileName deflate(Path c9rPath) { + String longFileName = c9rPath.getFileName().toString(); byte[] longFileNameBytes = longFileName.getBytes(UTF_8); byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); String shortName = BASE64.encode(hash) + DEFLATED_FILE_SUFFIX; - Path result = canonicalFileName.resolveSibling(shortName); - String cachedLongName = longNames.getIfPresent(shortName); - if (cachedLongName == null) { - longNames.put(result, longFileName); - } else { - assert cachedLongName.equals(longFileName); - } - return result; - } - - public Optional getCached(Path c9sPath) { - String longName = longNames.getIfPresent(c9sPath); - if (longName != null) { - return Optional.of(new DeflatedFileName(c9sPath, longName)); - } else { - return Optional.empty(); - } + Path c9sPath = c9rPath.resolveSibling(shortName); + longNames.put(c9sPath, longFileName); + return new DeflatedFileName(c9sPath, longFileName); } public class DeflatedFileName { diff --git a/src/main/java/org/cryptomator/cryptofs/Symlinks.java b/src/main/java/org/cryptomator/cryptofs/Symlinks.java index 0a7c141a..374fd7cf 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -42,12 +42,12 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib if (target.toString().length() > Constants.MAX_SYMLINK_LENGTH) { throw new IOException("path length limit exceeded."); } - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).getSymlinkFilePath(); + CiphertextFilePath ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag); ByteBuffer content = UTF_8.encode(target.toString()); - Files.createDirectory(ciphertextSymlinkFile.getParent()); - openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content); - longFileNameProvider.getCached(ciphertextSymlinkFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.createDirectory(ciphertextFilePath.getRawPath()); + openCryptoFiles.writeCiphertextFile(ciphertextFilePath.getSymlinkFilePath(), openOptions, content); + ciphertextFilePath.persistLongFileName(); } public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException { diff --git a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java index 89e29215..fadd55d3 100644 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java @@ -21,20 +21,22 @@ public class ConflictResolverTest { + private Path tmpDir; private LongFileNameProvider longFileNameProvider; + private CryptoPathMapper cryptoPathMapper; private Cryptor cryptor; private FileNameCryptor filenameCryptor; private ConflictResolver conflictResolver; private String dirId; - private Path tmpDir; @BeforeEach public void setup(@TempDir Path tmpDir) { this.tmpDir = tmpDir; this.longFileNameProvider = Mockito.mock(LongFileNameProvider.class); + this.cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); this.cryptor = Mockito.mock(Cryptor.class); this.filenameCryptor = Mockito.mock(FileNameCryptor.class); - this.conflictResolver = new ConflictResolver(longFileNameProvider, cryptor); + this.conflictResolver = new ConflictResolver(longFileNameProvider, cryptoPathMapper, cryptor); this.dirId = "foo"; Mockito.when(cryptor.fileNameCryptor()).thenReturn(filenameCryptor); @@ -122,11 +124,15 @@ public void testResolveByRenamingRegularFile() throws IOException { Mockito.when(longFileNameProvider.isDeflated(Mockito.eq(canonicalName))).thenReturn(false); Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq("FooBar=="), Mockito.any())).thenReturn("cleartext.txt"); - Mockito.when(filenameCryptor.encryptFilename(Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"), Mockito.any())).thenReturn("BarFoo=="); + Path resolvedC9rPath = canonicalPath.resolveSibling("BarFoo==.c9r"); + CiphertextFilePath alternativeCiphertextPath = Mockito.mock(CiphertextFilePath.class); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(Mockito.eq(tmpDir), Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"))).thenReturn(alternativeCiphertextPath); + Mockito.when(alternativeCiphertextPath.getRawPath()).thenReturn(resolvedC9rPath); Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - Assertions.assertEquals("BarFoo==.c9r", result.getFileName().toString()); + Mockito.verify(alternativeCiphertextPath).persistLongFileName(); + Assertions.assertEquals(resolvedC9rPath, result); Assertions.assertFalse(Files.exists(conflictingPath)); Assertions.assertTrue(Files.exists(result)); } @@ -144,15 +150,15 @@ public void testResolveByRenamingShortenedFile() throws IOException { Mockito.when(longFileNameProvider.isDeflated(canonicalName)).thenReturn(true); Mockito.when(longFileNameProvider.inflate(canonicalPath)).thenReturn(inflatedName); Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq(inflatedName), Mockito.any())).thenReturn("cleartext.txt"); - String resolvedCiphertext = Strings.repeat("b", Constants.SHORT_NAMES_MAX_LENGTH + 1); - Path resolvedInflatedPath = canonicalPath.resolveSibling(resolvedCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX); - Path resolvedDeflatedPath = canonicalPath.resolveSibling("BarFoo==.c9s"); - Mockito.when(filenameCryptor.encryptFilename(Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"), Mockito.any())).thenReturn(resolvedCiphertext); - Mockito.when(longFileNameProvider.deflate(resolvedInflatedPath)).thenReturn(resolvedDeflatedPath); + Path resolvedC9sPath = canonicalPath.resolveSibling("BarFoo==.c9s"); + CiphertextFilePath alternativeCiphertextPath = Mockito.mock(CiphertextFilePath.class); + Mockito.when(cryptoPathMapper.getCiphertextFilePath(Mockito.eq(tmpDir), Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"))).thenReturn(alternativeCiphertextPath); + Mockito.when(alternativeCiphertextPath.getRawPath()).thenReturn(resolvedC9sPath); Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Assertions.assertEquals(resolvedDeflatedPath, result); + + Mockito.verify(alternativeCiphertextPath).persistLongFileName(); + Assertions.assertEquals(resolvedC9sPath, result); Assertions.assertFalse(Files.exists(conflictingPath)); Assertions.assertTrue(Files.exists(result)); } diff --git a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java index d28f7b35..853d2440 100644 --- a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java @@ -54,16 +54,16 @@ public void testIsDeflated() { public void testDeflateAndInflate(@TempDir Path tmpPath) throws IOException { String orig = "longName"; LongFileNameProvider prov1 = new LongFileNameProvider(readonlyFlag); - Path deflated = prov1.deflate(tmpPath.resolve(orig)); - String inflated1 = prov1.inflate(deflated); + LongFileNameProvider.DeflatedFileName deflated = prov1.deflate(tmpPath.resolve(orig)); + String inflated1 = prov1.inflate(deflated.c9sPath); Assertions.assertEquals(orig, inflated1); Assertions.assertEquals(0, countFiles(tmpPath)); - prov1.getCached(deflated).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + deflated.persist(); Assertions.assertEquals(1, countFiles(tmpPath)); LongFileNameProvider prov2 = new LongFileNameProvider(readonlyFlag); - String inflated2 = prov2.inflate(deflated); + String inflated2 = prov2.inflate(deflated.c9sPath); Assertions.assertEquals(orig, inflated2); } @@ -92,13 +92,11 @@ public void testPerstistCachedFailsOnReadOnlyFileSystems(@TempDir Path tmpPath) String orig = "longName"; Path canonicalFileName = tmpPath.resolve(orig); - Path c9sFile = prov.deflate(canonicalFileName); - Optional cachedFileName = prov.getCached(c9sFile); + LongFileNameProvider.DeflatedFileName deflated = prov.deflate(canonicalFileName); - Assertions.assertTrue(cachedFileName.isPresent()); Mockito.doThrow(new ReadOnlyFileSystemException()).when(readonlyFlag).assertWritable(); Assertions.assertThrows(ReadOnlyFileSystemException.class, () -> { - cachedFileName.get().persist(); + deflated.persist(); }); } From 7644413fc52f14b2dcd8198647400a477f8bb91d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Sep 2019 13:01:39 +0200 Subject: [PATCH 28/62] don't use exception from migrator package --- .../java/org/cryptomator/cryptofs/LongFileNameProvider.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index cd104a62..42df38a4 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -13,7 +13,6 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.migration.v7.UninflatableFileException; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import javax.inject.Inject; @@ -55,7 +54,7 @@ public String load(Path c9sPath) throws IOException { Path longNameFile = c9sPath.resolve(INFLATED_FILE_NAME); try (SeekableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.READ)) { if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { - throw new UninflatableFileException("Unexpectedly large file: " + longNameFile); + throw new IOException("Unexpectedly large file: " + longNameFile); } assert ch.size() <= MAX_FILENAME_BUFFER_SIZE; ByteBuffer buf = ByteBuffer.allocate((int) ch.size()); From 865262b1aac25e02ebf3d9a0ce52bbc3c29a38c9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Sep 2019 13:03:47 +0200 Subject: [PATCH 29/62] delete name.c9s files if they are no longer being used --- .../cryptofs/CiphertextFilePath.java | 4 ++ .../cryptofs/CryptoFileSystemImpl.java | 52 +++++++++++-------- .../cryptofs/CryptoFileSystemImplTest.java | 8 ++- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java index d54b73e4..152d7643 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -33,6 +33,10 @@ public Path getDirFilePath() { public Path getSymlinkFilePath() { return path.resolve(Constants.SYMLINK_FILE_NAME); } + + public Path getInflatedNamePath() { + return path.resolve(Constants.INFLATED_FILE_NAME); + } @Override public int hashCode() { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index 88403c6b..650e7280 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -533,7 +533,11 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) { Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); - ciphertextTarget.persistLongFileName(); + if (ciphertextTarget.isShortened()) { + ciphertextTarget.persistLongFileName(); + } else { + Files.deleteIfExists(ciphertextTarget.getInflatedNamePath()); // no longer needed if not shortened + } twoPhaseMove.commit(); } } @@ -559,31 +563,35 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // Since we only rename the directory file, all ciphertext paths of subresources stay the same. // Hence there is no need to re-map OpenCryptoFile entries. + if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { + // check if not attempting to move atomically: + if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { + throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks."); + } + // check if dir is empty: + Path oldCiphertextDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; + boolean oldCiphertextDirExists = true; + try (DirectoryStream ds = Files.newDirectoryStream(oldCiphertextDir)) { + if (ds.iterator().hasNext()) { + throw new DirectoryNotEmptyException(cleartextTarget.toString()); + } + } catch (NoSuchFileException e) { + oldCiphertextDirExists = false; + } + // cleanup dir to be replaced: + if (oldCiphertextDirExists) { + Files.walkFileTree(oldCiphertextDir, DeletingFileVisitor.INSTANCE); + } + } + + // no exceptions until this point, so MOVE: CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); - if (!ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { - // try to move, don't replace: - Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + if (ciphertextTarget.isShortened()) { ciphertextTarget.persistLongFileName(); - } else if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { - // replace atomically (impossible): - assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); - throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks."); } else { - // move and replace (if dir is empty): - assert ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING); - assert !ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE); - if (Files.exists(ciphertextTarget.getRawPath())) { - Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; - try (DirectoryStream ds = Files.newDirectoryStream(ciphertextTargetDir)) { - if (ds.iterator().hasNext()) { - throw new DirectoryNotEmptyException(cleartextTarget.toString()); - } - } - Files.delete(ciphertextTargetDir); - } - Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); - ciphertextTarget.persistLongFileName(); + Files.deleteIfExists(ciphertextTarget.getInflatedNamePath()); // no longer needed if not shortened } dirIdProvider.move(ciphertextSource.getDirFilePath(), ciphertextTarget.getDirFilePath()); cryptoPathMapper.invalidatePathMapping(cleartextSource); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 4aa5b51b..002113b1 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -436,6 +436,7 @@ public class CopyAndMove { private final Path ciphertextSourceDirFile = mock(Path.class, "d/00/00/source.c9r/dir.c9r"); private final Path ciphertextSourceDir = mock(Path.class, "d/00/SOURCE/"); private final Path ciphertextDestinationFile = mock(Path.class, "d/00/00/dest.c9r"); + private final Path ciphertextDestinationLongNameFile = mock(Path.class, "d/00/00/dest.c9r/name.c9s"); private final Path ciphertextDestinationDirFile = mock(Path.class, "d/00/00/dest.c9r/dir.c9r"); private final Path ciphertextDestinationDir = mock(Path.class, "d/00/DEST/"); private final FileSystem physicalFs = mock(FileSystem.class); @@ -451,10 +452,12 @@ public void setup() throws IOException { when(ciphertextDestination.getFilePath()).thenReturn(ciphertextDestinationFile); when(ciphertextDestination.getSymlinkFilePath()).thenReturn(ciphertextDestinationFile); when(ciphertextDestination.getDirFilePath()).thenReturn(ciphertextDestinationDirFile); + when(ciphertextDestination.getInflatedNamePath()).thenReturn(ciphertextDestinationLongNameFile); when(ciphertextSourceFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextSourceDir.getFileSystem()).thenReturn(physicalFs); when(ciphertextDestinationFile.getFileSystem()).thenReturn(physicalFs); + when(ciphertextDestinationLongNameFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextDestinationDirFile.getFileSystem()).thenReturn(physicalFs); when(ciphertextDestinationDir.getFileSystem()).thenReturn(physicalFs); when(physicalFs.provider()).thenReturn(physicalFsProv); @@ -555,6 +558,9 @@ public void moveDirectoryDontReplaceExisting() throws IOException { public void moveDirectoryReplaceExisting() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenReturn(CiphertextFileType.DIRECTORY); + BasicFileAttributes destinationDirAttrs = mock(BasicFileAttributes.class); + when(physicalFsProv.readAttributes(Mockito.same(ciphertextDestinationDir), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(destinationDirAttrs); + when(destinationDirAttrs.isDirectory()).thenReturn(true); DirectoryStream ds = mock(DirectoryStream.class); Iterator iter = mock(Iterator.class); when(physicalFsProv.newDirectoryStream(Mockito.same(ciphertextDestinationDir), Mockito.any())).thenReturn(ds); @@ -564,7 +570,7 @@ public void moveDirectoryReplaceExisting() throws IOException { inTest.move(cleartextSource, cleartextDestination, StandardCopyOption.REPLACE_EXISTING); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).delete(ciphertextDestinationDir); + verify(physicalFsProv).deleteIfExists(ciphertextDestinationDir); verify(physicalFsProv).move(ciphertextSourceFile, ciphertextDestinationFile, StandardCopyOption.REPLACE_EXISTING); verify(dirIdProvider).move(ciphertextSourceDirFile, ciphertextDestinationDirFile); verify(cryptoPathMapper).invalidatePathMapping(cleartextSource); From 774727d9f4f4618e7c062190b0c85dedf226b4fe Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 5 Sep 2019 17:29:10 +0200 Subject: [PATCH 30/62] fixed various bugs when moving directories --- .../cryptofs/CryptoFileSystemImpl.java | 7 ++++--- .../cryptomator/cryptofs/CryptoPathMapper.java | 8 ++++++++ .../cryptofs/DeletingFileVisitor.java | 17 ++++++++++++++++- .../cryptofs/CryptoFileSystemImplTest.java | 13 ++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index 650e7280..79a68d72 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -563,6 +563,8 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // Since we only rename the directory file, all ciphertext paths of subresources stay the same. // Hence there is no need to re-map OpenCryptoFile entries. + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // check if not attempting to move atomically: if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) { @@ -582,11 +584,10 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge if (oldCiphertextDirExists) { Files.walkFileTree(oldCiphertextDir, DeletingFileVisitor.INSTANCE); } + Files.walkFileTree(ciphertextTarget.getRawPath(), DeletingFileVisitor.INSTANCE); } // no exceptions until this point, so MOVE: - CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); - CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); if (ciphertextTarget.isShortened()) { ciphertextTarget.persistLongFileName(); @@ -594,7 +595,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge Files.deleteIfExists(ciphertextTarget.getInflatedNamePath()); // no longer needed if not shortened } dirIdProvider.move(ciphertextSource.getDirFilePath(), ciphertextTarget.getDirFilePath()); - cryptoPathMapper.invalidatePathMapping(cleartextSource); + cryptoPathMapper.movePathMapping(cleartextSource, cleartextTarget); } CryptoFileStore getFileStore() { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index a0643f14..31408e41 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -134,6 +134,14 @@ private String getCiphertextFileName(DirIdAndName dirIdAndName) { public void invalidatePathMapping(CryptoPath cleartextPath) { ciphertextDirectories.invalidate(cleartextPath); } + + public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) { + CiphertextDirectory cachedValue = ciphertextDirectories.getIfPresent(cleartextSrc); + if (cachedValue != null) { + ciphertextDirectories.put(cleartextDst, cachedValue); + ciphertextDirectories.invalidate(cleartextSrc); + } + } public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException { assert cleartextPath.isAbsolute(); diff --git a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java index 495ce3f4..41e68b21 100644 --- a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java +++ b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; @@ -20,7 +21,12 @@ import java.util.EnumSet; import java.util.Set; -import static java.nio.file.attribute.PosixFilePermission.*; +import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; +import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; class DeletingFileVisitor extends SimpleFileVisitor { @@ -31,6 +37,15 @@ class DeletingFileVisitor extends SimpleFileVisitor { private DeletingFileVisitor() { } + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + if (exc instanceof NoSuchFileException) { + return FileVisitResult.SKIP_SUBTREE; + } else { + throw exc; + } + } + @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { forceDeleteIfExists(file); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 002113b1..76927e87 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -550,7 +550,7 @@ public void moveDirectoryDontReplaceExisting() throws IOException { verify(readonlyFlag).assertWritable(); verify(physicalFsProv).move(ciphertextSourceFile, ciphertextDestinationFile, option1, option2); verify(dirIdProvider).move(ciphertextSourceDirFile, ciphertextDestinationDirFile); - verify(cryptoPathMapper).invalidatePathMapping(cleartextSource); + verify(cryptoPathMapper).movePathMapping(cleartextSource, cleartextDestination); } @Test @@ -558,11 +558,13 @@ public void moveDirectoryDontReplaceExisting() throws IOException { public void moveDirectoryReplaceExisting() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.DIRECTORY); when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenReturn(CiphertextFileType.DIRECTORY); - BasicFileAttributes destinationDirAttrs = mock(BasicFileAttributes.class); - when(physicalFsProv.readAttributes(Mockito.same(ciphertextDestinationDir), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(destinationDirAttrs); - when(destinationDirAttrs.isDirectory()).thenReturn(true); + BasicFileAttributes dirAttr = mock(BasicFileAttributes.class); + when(physicalFsProv.readAttributes(Mockito.same(ciphertextDestinationFile), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(dirAttr); + when(physicalFsProv.readAttributes(Mockito.same(ciphertextDestinationDir), Mockito.same(BasicFileAttributes.class), Mockito.any())).thenReturn(dirAttr); + when(dirAttr.isDirectory()).thenReturn(true); DirectoryStream ds = mock(DirectoryStream.class); Iterator iter = mock(Iterator.class); + when(physicalFsProv.newDirectoryStream(Mockito.same(ciphertextDestinationFile), Mockito.any())).thenReturn(ds); when(physicalFsProv.newDirectoryStream(Mockito.same(ciphertextDestinationDir), Mockito.any())).thenReturn(ds); when(ds.iterator()).thenReturn(iter); when(iter.hasNext()).thenReturn(false); @@ -571,9 +573,10 @@ public void moveDirectoryReplaceExisting() throws IOException { verify(readonlyFlag).assertWritable(); verify(physicalFsProv).deleteIfExists(ciphertextDestinationDir); + verify(physicalFsProv).deleteIfExists(ciphertextDestinationFile); verify(physicalFsProv).move(ciphertextSourceFile, ciphertextDestinationFile, StandardCopyOption.REPLACE_EXISTING); verify(dirIdProvider).move(ciphertextSourceDirFile, ciphertextDestinationDirFile); - verify(cryptoPathMapper).invalidatePathMapping(cleartextSource); + verify(cryptoPathMapper).movePathMapping(cleartextSource, cleartextDestination); } @Test From ffc72232bd9cf4a672b6533910fcc772d4fef540 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 6 Sep 2019 00:12:52 +0200 Subject: [PATCH 31/62] fixed tagged deployments --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d7e490d..80de6ad0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,10 @@ cache: - $HOME/.m2 deploy: - provider: script - script: mvn clean versions:set -DnewVersion=${TRAVIS_TAG} deploy -DskipTests -Prelease - skip_cleanup: true + script: + - mvn versions:set -DnewVersion=${TRAVIS_TAG} + - mvn clean deploy -DskipTests -Prelease + skip_cleanup: false on: repo: cryptomator/cryptofs tags: true From cf548b25c1768b1e464f021383147b3dd3353fa2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 6 Sep 2019 00:24:04 +0200 Subject: [PATCH 32/62] fixed tagged deployments --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 80de6ad0..3f39dd0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ install: - echo "MAVEN_OPTS='-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true'" > ~/.mavenrc before_script: - mvn -B --update-snapshots dependency-check:check -Pdependency-check +- if [ -n "$TRAVIS_TAG" ]; then mvn versions:set -DnewVersion=${TRAVIS_TAG}; fi script: - mvn -B clean test jacoco:report verify -Pcoverage -Dorg.slf4j.simpleLogger.defaultLogLevel=debug - | @@ -26,10 +27,8 @@ cache: - $HOME/.m2 deploy: - provider: script - script: - - mvn versions:set -DnewVersion=${TRAVIS_TAG} - - mvn clean deploy -DskipTests -Prelease - skip_cleanup: false + script: mvn clean deploy -DskipTests -Prelease + skip_cleanup: true on: repo: cryptomator/cryptofs tags: true From 98ff20c12fb40fea88e2d0eb5a6c0ee904bf471d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Sep 2019 19:25:35 +0200 Subject: [PATCH 33/62] Report progress during long-running vault migrations --- pom.xml | 4 +- .../cryptofs/CryptoFileSystemProvider.java | 4 +- .../cryptofs/DeletingFileVisitor.java | 2 +- .../cryptofs/migration/MigrationModule.java | 14 ------- .../cryptofs/migration/Migrators.java | 8 ++-- .../api/MigrationProgressListener.java | 33 +++++++++++++++++ .../cryptofs/migration/api/Migrator.java | 24 ++++++------ .../migration/v6/Version6Migrator.java | 7 +++- .../migration/v7/Version7Migrator.java | 37 +++++++++++++++++-- .../cryptofs/migration/MigratorsTest.java | 12 +++--- .../migration/v7/Version7MigratorTest.java | 8 ++++ 11 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java diff --git a/pom.xml b/pom.xml index ffa560a7..2977686d 100644 --- a/pom.xml +++ b/pom.xml @@ -16,10 +16,10 @@ 1.2.2 2.24 - 28.0-jre + 28.1-jre 1.7.28 - 5.5.1 + 5.5.2 3.0.0 2.1 UTF-8 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 4e06d511..d750c69f 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -255,16 +255,18 @@ public CryptoFileSystem newFileSystem(URI uri, Map rawProperties) thr return fileSystems.create(this, parsedUri.pathToVault(), properties); } + @Deprecated private void migrateFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException { if (Migrators.get().needsMigration(parsedUri.pathToVault(), properties.masterkeyFilename())) { if (properties.migrateImplicitly()) { - Migrators.get().migrate(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase()); + Migrators.get().migrate(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase(), (state, progress) -> {}); } else { throw new FileSystemNeedsMigrationException(parsedUri.pathToVault()); } } } + @Deprecated private void initializeFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws NotDirectoryException, IOException, NoSuchFileException { if (!CryptoFileSystemProvider.containsVault(parsedUri.pathToVault(), properties.masterkeyFilename())) { if (properties.initializeImplicitly()) { diff --git a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java index 41e68b21..bf0b107a 100644 --- a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java +++ b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java @@ -28,7 +28,7 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; -class DeletingFileVisitor extends SimpleFileVisitor { +public class DeletingFileVisitor extends SimpleFileVisitor { public static final DeletingFileVisitor INSTANCE = new DeletingFileVisitor(); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java index 41a83dd5..d69c2efc 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java @@ -49,20 +49,6 @@ Migrator provideVersion7Migrator(Version7Migrator migrator) { return migrator; } - // @Provides - // @IntoMap - // @MigratorKey(Migration.SIX_TO_SEVEN) - // Migrator provideVersion7Migrator(Version7Migrator migrator) { - // return migrator; - // } - // - // @Provides - // @IntoMap - // @MigratorKey(Migration.FIVE_TO_SEVEN) - // Migrator provideVersion7Migrator(Version6Migrator v6Migrator, Version7Migrator v7Migrator) { - // return v6Migrator.andThen(v7Migrator); - // } - @Documented @Target(METHOD) @Retention(RUNTIME) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index f42d17dc..21072706 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -16,6 +16,7 @@ import javax.inject.Inject; import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; import org.cryptomator.cryptolib.Cryptors; @@ -31,7 +32,7 @@ *

  * 
  * if (Migrators.get().{@link #needsMigration(Path, String) needsMigration(pathToVault, masterkeyFileName)}) {
- * 	Migrators.get().{@link #migrate(Path, String, CharSequence) migrate(pathToVault, masterkeyFileName, passphrase)};
+ * 	Migrators.get().{@link #migrate(Path, String, CharSequence, MigrationProgressListener) migrate(pathToVault, masterkeyFileName, passphrase, migrationProgressListener)};
  * }
  * 
  * 
@@ -88,14 +89,14 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws * @throws InvalidPassphraseException If the passphrase could not be used to unlock the vault * @throws IOException if an I/O error occurs migrating the vault */ - public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase) throws NoApplicableMigratorException, InvalidPassphraseException, IOException { + public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException { Path masterKeyPath = pathToVault.resolve(masterkeyFilename); byte[] keyFileContents = Files.readAllBytes(masterKeyPath); KeyFile keyFile = KeyFile.parse(keyFileContents); try { Migrator migrator = findApplicableMigrator(keyFile.getVersion()).orElseThrow(NoApplicableMigratorException::new); - migrator.migrate(pathToVault, masterkeyFilename, passphrase); + migrator.migrate(pathToVault, masterkeyFilename, passphrase, progressListener); } catch (UnsupportedVaultFormatException e) { // might be a tampered masterkey file, as this exception is also thrown if the vault version MAC is not authentic. throw new IllegalStateException("Vault version checked beforehand but not supported by migrator."); @@ -103,7 +104,6 @@ public void migrate(Path pathToVault, String masterkeyFilename, CharSequence pas } private Optional findApplicableMigrator(int version) { - // TODO return "5->6->7" instead of "5->6" and "6->7", if possible return migrators.entrySet().stream().filter(entry -> entry.getKey().isApplicable(version)).map(Map.Entry::getValue).findAny(); } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java new file mode 100644 index 00000000..9fe9ef68 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java @@ -0,0 +1,33 @@ +package org.cryptomator.cryptofs.migration.api; + +@FunctionalInterface +public interface MigrationProgressListener { + + /** + * Called on every step during migration that might change the progress. + * + * @param state Current state of the migration + * @param progress Progress that should be between 0.0 and 1.0 but due to inaccurate estimations it might even be 1.1 + */ + void update(ProgressState state, double progress); + + enum ProgressState { + /** + * Migration recently started. The progress can't be calculated yet. + */ + INITIALIZING, + + /** + * Migration is running and progress can be calculated. + *

+ * Any long-running tasks should (if possible) happen in this state. + */ + MIGRATING, + + /** + * Cleanup after success or failure is running. Remaining time is in unknown. + */ + FINALIZING + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java index 0319e00f..3d6790f6 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java @@ -26,19 +26,21 @@ public interface Migrator { * @throws UnsupportedVaultFormatException * @throws IOException */ - void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException; + default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + migrate(vaultRoot, masterkeyFilename, passphrase, (state, progress) -> {}); + } /** - * Chains this migrator with a consecutive migrator. - * - * @param nextMigration The next migrator able to read the vault format created by this migrator. - * @return A combined migrator performing both steps in order. + * Performs the migration this migrator is built for. + * + * @param vaultRoot + * @param masterkeyFilename + * @param passphrase + * @param progressListener + * @throws InvalidPassphraseException + * @throws UnsupportedVaultFormatException + * @throws IOException */ - default Migrator andThen(Migrator nextMigration) { - return (Path vaultRoot, String masterkeyFilename, CharSequence passphrase) -> { - migrate(vaultRoot, masterkeyFilename, passphrase); - nextMigration.migrate(vaultRoot, masterkeyFilename, passphrase); - }; - } + void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException; } diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java index 82121960..dae0b087 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java @@ -17,6 +17,7 @@ import org.cryptomator.cryptofs.BackupUtil; import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; @@ -38,8 +39,9 @@ public Version6Migrator(CryptorProvider cryptorProvider) { } @Override - public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { LOG.info("Upgrading {} from version 5 to version 6.", vaultRoot); + progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0); Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile); KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); @@ -48,6 +50,9 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); + + progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0); + // rewrite masterkey file with normalized passphrase: byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), 6).serialize(); Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index e0453100..6b10aea9 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.BackupUtil; import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.DeletingFileVisitor; +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; @@ -29,6 +31,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.Optional; +import java.util.concurrent.atomic.LongAdder; public class Version7Migrator implements Migrator { @@ -42,8 +45,9 @@ public Version7Migrator(CryptorProvider cryptorProvider) { } @Override - public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { + public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException { LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot); + progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0); Path masterkeyFile = vaultRoot.resolve(masterkeyFilename); byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile); KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); @@ -53,9 +57,15 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); - migrateFileNames(vaultRoot); + long toBeMigrated = countFileNames(vaultRoot); + if (toBeMigrated > 0) { + migrateFileNames(vaultRoot, progressListener, toBeMigrated); + } + + progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0); - // TODO remove deprecated .lng from /m/ + // remove deprecated /m/ directory + Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE); // rewrite masterkey file with normalized passphrase: byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 7).serialize(); @@ -64,13 +74,32 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp } LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot); } + + private long countFileNames(Path vaultRoot) throws IOException { + LongAdder counter = new LongAdder(); + Path dataDir = vaultRoot.resolve("d"); + Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + counter.increment(); + return FileVisitResult.CONTINUE; + } + }); + return counter.sum(); + } - private void migrateFileNames(Path vaultRoot) throws IOException { + private void migrateFileNames(Path vaultRoot, MigrationProgressListener progressListener, long totalFiles) throws IOException { + assert totalFiles > 0; Path dataDir = vaultRoot.resolve("d"); Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { + + long migratedFiles = 0; @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + migratedFiles++; + progressListener.update(MigrationProgressListener.ProgressState.MIGRATING, (double) migratedFiles / totalFiles); final Optional migration; try { migration = FilePathMigration.parse(vaultRoot, file); diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java index d0e5b429..df8f3e29 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java @@ -5,6 +5,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; @@ -76,21 +77,22 @@ public void testNeedsNoMigration() throws IOException { public void testMigrateWithoutMigrators() throws IOException { Migrators migrators = new Migrators(Collections.emptyMap()); Assertions.assertThrows(NoApplicableMigratorException.class, () -> { - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret"); + migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}); }); } @Test @SuppressWarnings("deprecation") public void testMigrate() throws NoApplicableMigratorException, InvalidPassphraseException, IOException { + MigrationProgressListener listener = Mockito.mock(MigrationProgressListener.class); Migrator migrator = Mockito.mock(Migrator.class); Migrators migrators = new Migrators(new HashMap() { { put(Migration.ZERO_TO_ONE, migrator); } }); - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret"); - Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret"); + migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", listener); + Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret", listener); } @Test @@ -102,9 +104,9 @@ public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorExcep put(Migration.ZERO_TO_ONE, migrator); } }); - Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret"); + Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); Assertions.assertThrows(IllegalStateException.class, () -> { - migrators.migrate(pathToVault, "masterkey.cryptomator", "secret"); + migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}); }); } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index 9f020cff..3fe6a345 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -61,4 +61,12 @@ public void testKeyfileGetsUpdates() throws IOException { Assertions.assertEquals(7, afterMigration.getVersion()); } + @Test + public void testMDirectoryGetsDeleted() throws IOException { + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + Assertions.assertFalse(Files.exists(metaDir)); + } + } From 0bf49e5fc8d27938ce808f890ca1559096b8a891 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Sep 2019 20:59:44 +0200 Subject: [PATCH 34/62] new API to export and restore the encrypted key stored inside a masterkey file --- pom.xml | 2 +- .../cryptofs/CryptoFileSystemProvider.java | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2977686d..56eb1608 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ - 1.2.2 + 1.3.0-beta1 2.24 28.1-jre 1.7.28 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index d750c69f..cc74f0ec 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -230,6 +230,45 @@ public static void changePassphrase(Path pathToVault, String masterkeyFilename, Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE); } + /** + * Exports the raw key for backup purposes or external key management. + * + * @param pathToVault Vault directory + * @param masterkeyFilename Name of the masterkey file + * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) + * @param passphrase Current passphrase + * @return A 64 byte array consisting of 32 byte aes key and 32 byte mac key + * @since 1.9.0 + */ + public static byte[] exportRawKey(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException { + String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC); + Path masterKeyPath = pathToVault.resolve(masterkeyFilename); + byte[] masterKeyBytes = Files.readAllBytes(masterKeyPath); + return Cryptors.exportRawKey(CRYPTOR_PROVIDER, masterKeyBytes, pepper, normalizedPassphrase); + } + + /** + * Imports a raw key from backup or external key management. + * + * @param pathToVault Vault directory + * @param masterkeyFilename Name of the masterkey file + * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) + * @param passphrase Future passphrase + * @return A 64 byte array consisting of 32 byte aes key and 32 byte mac key + * @since 1.9.0 + */ + public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byte[] rawKey, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException { + String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC); + byte[] masterKeyBytes = Cryptors.restoreRawKey(CRYPTOR_PROVIDER, rawKey, pepper, normalizedPassphrase, Constants.VAULT_VERSION); + Path masterKeyPath = pathToVault.resolve(masterkeyFilename); + if (Files.exists(masterKeyPath)) { + byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); + Path backupKeyPath = pathToVault.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); + Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); + } + Files.write(masterKeyPath, masterKeyBytes, CREATE_NEW, WRITE); + } + /** * @deprecated only for testing */ From a3fd45df5e04a230e5df12a4a1edec0c13e924f5 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Sep 2019 21:24:59 +0200 Subject: [PATCH 35/62] fixed javadoc --- .../java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index cc74f0ec..b78137bf 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -254,7 +254,6 @@ public static byte[] exportRawKey(Path pathToVault, String masterkeyFilename, by * @param masterkeyFilename Name of the masterkey file * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable) * @param passphrase Future passphrase - * @return A 64 byte array consisting of 32 byte aes key and 32 byte mac key * @since 1.9.0 */ public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byte[] rawKey, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException { From ca063e96ee4ae5e691a905c6c79869e72863938a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 27 Sep 2019 21:25:09 +0200 Subject: [PATCH 36/62] updated CI config --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f39dd0f..7efb2590 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: xenial +dist: bionic language: java sudo: false jdk: @@ -14,10 +14,13 @@ before_script: - mvn -B --update-snapshots dependency-check:check -Pdependency-check - if [ -n "$TRAVIS_TAG" ]; then mvn versions:set -DnewVersion=${TRAVIS_TAG}; fi script: -- mvn -B clean test jacoco:report verify -Pcoverage -Dorg.slf4j.simpleLogger.defaultLogLevel=debug - | - if [[ "$TRAVIS_BRANCH" =~ ^release/.* ]]; then - mvn -B javadoc:jar; + if [[ -n "$TRAVIS_TAG" ]]; then + mvn clean install jacoco:report verify -Pcoverage,release + elif [[ "$TRAVIS_BRANCH" =~ ^release/.* ]]; then + mvn clean install jacoco:report verify -Pcoverage,release + else + mvn clean test jacoco:report verify -Pcoverage fi after_success: - curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/4.0.3/codacy-coverage-reporter-4.0.3-assembly.jar @@ -27,7 +30,7 @@ cache: - $HOME/.m2 deploy: - provider: script - script: mvn clean deploy -DskipTests -Prelease + script: mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true -Prelease skip_cleanup: true on: repo: cryptomator/cryptofs From b266343926751ab6e20616213c7431e0fc3da37b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 11 Oct 2019 09:14:23 +0200 Subject: [PATCH 37/62] Version7Migrator will now perform migration _after_ visiting a directory to avoid changes to elements while still iterating --- .../migration/v7/FilePathMigration.java | 4 ++ .../migration/v7/MigratingVisitor.java | 66 +++++++++++++++++++ .../migration/v7/Version7Migrator.java | 30 +-------- .../migration/v7/Version7MigratorTest.java | 45 +++++++++++++ 4 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v7/MigratingVisitor.java diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 3456b6c7..58f8246e 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -173,6 +173,10 @@ Path getTargetPath(String attemptSuffix) { } } } + + public Path getOldPath() { + return oldPath; + } // visible for testing String getOldCanonicalName() { diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/MigratingVisitor.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/MigratingVisitor.java new file mode 100644 index 00000000..551632c6 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/MigratingVisitor.java @@ -0,0 +1,66 @@ +package org.cryptomator.cryptofs.migration.v7; + +import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileVisitResult; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +class MigratingVisitor extends SimpleFileVisitor { + + private static final Logger LOG = LoggerFactory.getLogger(MigratingVisitor.class); + + private final Path vaultRoot; + private final MigrationProgressListener progressListener; + private final long estimatedTotalFiles; + + public MigratingVisitor(Path vaultRoot, MigrationProgressListener progressListener, long estimatedTotalFiles) { + this.vaultRoot = vaultRoot; + this.progressListener = progressListener; + this.estimatedTotalFiles = estimatedTotalFiles; + } + + private Collection migrationsInCurrentDir = new ArrayList<>(); + private long migratedFiles = 0; + + // Step 1: Collect files to be migrated + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + final Optional migration; + try { + migration = FilePathMigration.parse(vaultRoot, file); + } catch (UninflatableFileException e) { + LOG.warn("SKIP {} because inflation failed.", file); + return FileVisitResult.CONTINUE; + } + migration.ifPresent(migrationsInCurrentDir::add); + return FileVisitResult.CONTINUE; + } + + // Step 2: Only after visiting this dir, we will perform any changes to avoid "ConcurrentModificationExceptions" + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + for (FilePathMigration migration : migrationsInCurrentDir) { + migratedFiles++; + progressListener.update(MigrationProgressListener.ProgressState.MIGRATING, (double) migratedFiles / estimatedTotalFiles); + try { + Path migratedFile = migration.migrate(); + LOG.info("MOVED {} to {}", migration.getOldPath(), migratedFile); + } catch (FileAlreadyExistsException e) { + LOG.error("Failed to migrate " + migration.getOldPath() + " due to FileAlreadyExistsException. Already migrated on a different machine?.", e); + return FileVisitResult.TERMINATE; + } + } + migrationsInCurrentDir.clear(); + return FileVisitResult.CONTINUE; + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index 6b10aea9..cf5fcde7 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -20,7 +20,6 @@ import javax.inject.Inject; import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -30,7 +29,6 @@ import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; -import java.util.Optional; import java.util.concurrent.atomic.LongAdder; public class Version7Migrator implements Migrator { @@ -92,33 +90,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { private void migrateFileNames(Path vaultRoot, MigrationProgressListener progressListener, long totalFiles) throws IOException { assert totalFiles > 0; Path dataDir = vaultRoot.resolve("d"); - Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new SimpleFileVisitor() { - - long migratedFiles = 0; - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - migratedFiles++; - progressListener.update(MigrationProgressListener.ProgressState.MIGRATING, (double) migratedFiles / totalFiles); - final Optional migration; - try { - migration = FilePathMigration.parse(vaultRoot, file); - } catch (UninflatableFileException e) { - LOG.warn("SKIP {} because inflation failed.", file); - return FileVisitResult.CONTINUE; - } - if (migration.isPresent()) { - try { - Path migratedFile = migration.get().migrate(); - LOG.info("MOVED {} to {}", file, migratedFile); - } catch (FileAlreadyExistsException e) { - LOG.error("Failed to migrate " + file + " due to FileAlreadyExistsException. Already migrated?.", e); - return FileVisitResult.TERMINATE; - } - } - return FileVisitResult.CONTINUE; - } - }); + Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new MigratingVisitor(vaultRoot, progressListener, totalFiles)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java index 3fe6a345..aa3e758d 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -69,4 +69,49 @@ public void testMDirectoryGetsDeleted() throws IOException { Assertions.assertFalse(Files.exists(metaDir)); } + @Test + public void testMigrationOfNormalFile() throws IOException { + Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); + Files.createDirectories(dir); + Path fileBeforeMigration = dir.resolve("MZUWYZLOMFWWK==="); + Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r"); + Files.createFile(fileBeforeMigration); + + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + Assertions.assertFalse(Files.exists(fileBeforeMigration)); + Assertions.assertTrue(Files.exists(fileAfterMigration)); + } + + @Test + public void testMigrationOfNormalDirectory() throws IOException { + Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); + Files.createDirectories(dir); + Path fileBeforeMigration = dir.resolve("0MZUWYZLOMFWWK==="); + Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/dir.c9r"); + Files.createFile(fileBeforeMigration); + + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + Assertions.assertFalse(Files.exists(fileBeforeMigration)); + Assertions.assertTrue(Files.exists(fileAfterMigration)); + } + + @Test + public void testMigrationOfNormalSymlink() throws IOException { + Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG"); + Files.createDirectories(dir); + Path fileBeforeMigration = dir.resolve("1SMZUWYZLOMFWWK==="); + Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/symlink.c9r"); + Files.createFile(fileBeforeMigration); + + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + Assertions.assertFalse(Files.exists(fileBeforeMigration)); + Assertions.assertTrue(Files.exists(fileAfterMigration)); + } + } From bf6f040d0bbba15a098a42b22004002e5957629f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 13 Oct 2019 14:26:20 +0200 Subject: [PATCH 38/62] =?UTF-8?q?make=20sure=20that=20already-migrated=20f?= =?UTF-8?q?iles=20won't=20be=20affected=20by=20re-started=20migration=20fr?= =?UTF-8?q?om=20version=206=20=E2=86=92=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cryptofs/migration/v7/FilePathMigration.java | 6 +++++- .../cryptofs/migration/v7/FilePathMigrationTest.java | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java index 58f8246e..4a08097d 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -64,7 +64,11 @@ class FilePathMigration { public static Optional parse(Path vaultRoot, Path oldPath) throws IOException { final String oldFileName = oldPath.getFileName().toString(); final String canonicalOldFileName; - if (oldFileName.endsWith(OLD_SHORTENED_FILENAME_SUFFIX)) { + if (oldFileName.endsWith(NEW_REGULAR_SUFFIX) || oldFileName.endsWith(NEW_SHORTENED_SUFFIX)) { + // make sure to not match already migrated files + // (since BASE32 is a subset of BASE64, pure pattern matching could accidentally match those) + return Optional.empty(); + } else if (oldFileName.endsWith(OLD_SHORTENED_FILENAME_SUFFIX)) { Matcher matcher = OLD_SHORTENED_FILENAME_PATTERN.matcher(oldFileName); if (matcher.find()) { canonicalOldFileName = inflate(vaultRoot, matcher.group() + OLD_SHORTENED_FILENAME_SUFFIX); diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java index 40901245..e2962915 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -189,6 +189,8 @@ public void testInflate(String canonicalLongFileName, String metadataFilePath, S @ValueSource(strings = { "00/000000000000000000000000000000/.DS_Store", "00/000000000000000000000000000000/foo", + "00/000000000000000000000000000000/ORSXG5A=.c9r", // already migrated + "00/000000000000000000000000000000/ORSXG5A=.c9s", // already migrated "00/000000000000000000000000000000/ORSXG5A", // removed one char "00/000000000000000000000000000000/NTJDZUB3J5S25LGO7CD4TE5VOJCSW7H.lng", // removed one char }) From 42e8903e74c0ab61395bf849753b028f0bc415b1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 16 Oct 2019 17:39:20 +0200 Subject: [PATCH 39/62] update coverage reporter --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2d7e490d..23f60e25 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ script: mvn -B javadoc:jar; fi after_success: -- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/4.0.3/codacy-coverage-reporter-4.0.3-assembly.jar +- curl -o ~/codacy-coverage-reporter.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/6.0.7/codacy-coverage-reporter-6.0.7-assembly.jar - $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r target/site/jacoco/jacoco.xml cache: directories: From 3e710908f7d6ae38d5a9fdc22ef255c01e71de7f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 16 Oct 2019 17:40:10 +0200 Subject: [PATCH 40/62] updated cryptolib version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 56eb1608..1e79a5e5 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ - 1.3.0-beta1 + 1.3.0-beta2 2.24 28.1-jre 1.7.28 From 16fda96f1de44430098480d1dc234ba032867e6b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 22 Oct 2019 12:43:45 +0200 Subject: [PATCH 41/62] repackaged some classes --- .../cryptofs/CiphertextFilePath.java | 2 ++ .../org/cryptomator/cryptofs/Constants.java | 30 ------------------- .../cryptofs/CryptoFileSystemImpl.java | 7 +++-- .../cryptofs/CryptoFileSystemModule.java | 1 + .../cryptofs/CryptoFileSystemProvider.java | 1 + .../org/cryptomator/cryptofs/CryptoPath.java | 2 +- .../cryptofs/CryptoPathFactory.java | 2 +- .../cryptofs/CryptoPathMapper.java | 3 +- .../cryptofs/GlobToRegexConverter.java | 2 +- .../cryptofs/LongFileNameProvider.java | 6 ++-- .../cryptofs/RunnableThrowingException.java | 8 ----- .../org/cryptomator/cryptofs/Symlinks.java | 1 + .../cryptofs/common/Constants.java | 30 +++++++++++++++++++ .../{ => common}/DeletingFileVisitor.java | 4 +-- .../cryptofs/{ => common}/FinallyUtil.java | 4 +-- .../common/RunnableThrowingException.java | 8 +++++ .../cryptofs/{ => common}/StringUtils.java | 4 +-- .../SupplierThrowingException.java | 2 +- .../{ => dir}/CiphertextDirectoryDeleter.java | 12 +++++--- .../cryptofs/{ => dir}/ConflictResolver.java | 18 ++++++----- .../{ => dir}/CryptoDirectoryStream.java | 6 +++- .../{ => dir}/DirectoryStreamFactory.java | 19 +++++++----- .../{ => dir}/EncryptedNamePattern.java | 2 +- .../cryptofs/migration/Migrators.java | 2 +- .../migration/v6/Version6Migrator.java | 2 +- .../migration/v7/Version7Migrator.java | 4 +-- .../cryptofs/CryptoFileSystemImplTest.java | 4 +++ .../cryptofs/CryptoFileSystemUriTest.java | 1 + ...ptyCiphertextDirectoryIntegrationTest.java | 3 +- .../{ => common}/FinallyUtilTest.java | 4 ++- .../{ => dir}/ConflictResolverTest.java | 6 +++- .../CryptoDirectoryStreamIntegrationTest.java | 7 +++-- .../{ => dir}/CryptoDirectoryStreamTest.java | 8 ++++- .../{ => dir}/DirectoryStreamFactoryTest.java | 10 ++++++- .../{ => dir}/EncryptedNamePatternTest.java | 3 +- .../migration/v6/Version6MigratorTest.java | 2 +- 36 files changed, 142 insertions(+), 88 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptofs/Constants.java delete mode 100644 src/main/java/org/cryptomator/cryptofs/RunnableThrowingException.java create mode 100644 src/main/java/org/cryptomator/cryptofs/common/Constants.java rename src/main/java/org/cryptomator/cryptofs/{ => common}/DeletingFileVisitor.java (96%) rename src/main/java/org/cryptomator/cryptofs/{ => common}/FinallyUtil.java (94%) create mode 100644 src/main/java/org/cryptomator/cryptofs/common/RunnableThrowingException.java rename src/main/java/org/cryptomator/cryptofs/{ => common}/StringUtils.java (87%) rename src/main/java/org/cryptomator/cryptofs/{ => common}/SupplierThrowingException.java (73%) rename src/main/java/org/cryptomator/cryptofs/{ => dir}/CiphertextDirectoryDeleter.java (85%) rename src/main/java/org/cryptomator/cryptofs/{ => dir}/ConflictResolver.java (92%) rename src/main/java/org/cryptomator/cryptofs/{ => dir}/CryptoDirectoryStream.java (94%) rename src/main/java/org/cryptomator/cryptofs/{ => dir}/DirectoryStreamFactory.java (85%) rename src/main/java/org/cryptomator/cryptofs/{ => dir}/EncryptedNamePattern.java (94%) rename src/test/java/org/cryptomator/cryptofs/{ => common}/FinallyUtilTest.java (94%) rename src/test/java/org/cryptomator/cryptofs/{ => dir}/ConflictResolverTest.java (96%) rename src/test/java/org/cryptomator/cryptofs/{ => dir}/CryptoDirectoryStreamIntegrationTest.java (92%) rename src/test/java/org/cryptomator/cryptofs/{ => dir}/CryptoDirectoryStreamTest.java (93%) rename src/test/java/org/cryptomator/cryptofs/{ => dir}/DirectoryStreamFactoryTest.java (90%) rename src/test/java/org/cryptomator/cryptofs/{ => dir}/EncryptedNamePatternTest.java (90%) diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java index 152d7643..fdd2c2ed 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; + import java.nio.file.Path; import java.util.Objects; import java.util.Optional; diff --git a/src/main/java/org/cryptomator/cryptofs/Constants.java b/src/main/java/org/cryptomator/cryptofs/Constants.java deleted file mode 100644 index e0c3a147..00000000 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016, 2017 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptofs; - -public final class Constants { - - public static final int VAULT_VERSION = 7; - public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; - - static final String DATA_DIR_NAME = "d"; - static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 - static final String ROOT_DIR_ID = ""; - static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; - static final String DEFLATED_FILE_SUFFIX = ".c9s"; - static final String DIR_FILE_NAME = "dir.c9r"; - static final String SYMLINK_FILE_NAME = "symlink.c9r"; - static final String CONTENTS_FILE_NAME = "contents.c9r"; - static final String INFLATED_FILE_NAME = "name.c9s"; - - static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 - - static final String SEPARATOR = "/"; - -} diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index 79a68d72..ea12b6d3 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -13,6 +13,10 @@ import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; import org.cryptomator.cryptofs.attr.AttributeViewType; +import org.cryptomator.cryptofs.common.DeletingFileVisitor; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; +import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; @@ -56,13 +60,12 @@ 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; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.cryptomator.cryptofs.Constants.SEPARATOR; +import static org.cryptomator.cryptofs.common.Constants.SEPARATOR; @CryptoFileSystemScoped class CryptoFileSystemImpl extends CryptoFileSystem { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java index 90cd0eac..92d2eb15 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java @@ -7,6 +7,7 @@ import dagger.Module; import dagger.Provides; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.KeyFile; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index b78137bf..3b8cd20f 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java index 02b9b46d..7864cb72 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java @@ -24,7 +24,7 @@ import java.util.List; import java.util.Objects; -import static org.cryptomator.cryptofs.Constants.SEPARATOR; +import static org.cryptomator.cryptofs.common.Constants.SEPARATOR; public class CryptoPath implements Path { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java index 9a4f9090..469b026c 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java @@ -15,7 +15,7 @@ import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.Collectors.toList; import static java.util.stream.StreamSupport.stream; -import static org.cryptomator.cryptofs.Constants.SEPARATOR; +import static org.cryptomator.cryptofs.common.Constants.SEPARATOR; @CryptoFileSystemScoped class CryptoPathFactory { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 31408e41..8546b9f5 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -14,6 +14,7 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; import javax.inject.Inject; @@ -30,7 +31,7 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; -import static org.cryptomator.cryptofs.Constants.DATA_DIR_NAME; +import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; @CryptoFileSystemScoped public class CryptoPathMapper { diff --git a/src/main/java/org/cryptomator/cryptofs/GlobToRegexConverter.java b/src/main/java/org/cryptomator/cryptofs/GlobToRegexConverter.java index 2048a309..466edbd3 100644 --- a/src/main/java/org/cryptomator/cryptofs/GlobToRegexConverter.java +++ b/src/main/java/org/cryptomator/cryptofs/GlobToRegexConverter.java @@ -3,7 +3,7 @@ import javax.inject.Inject; import javax.inject.Singleton; -import static org.cryptomator.cryptofs.Constants.SEPARATOR; +import static org.cryptomator.cryptofs.common.Constants.SEPARATOR; @Singleton class GlobToRegexConverter { diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index 42df38a4..af25330d 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -28,11 +28,11 @@ import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.cryptomator.cryptofs.Constants.DEFLATED_FILE_SUFFIX; -import static org.cryptomator.cryptofs.Constants.INFLATED_FILE_NAME; +import static org.cryptomator.cryptofs.common.Constants.DEFLATED_FILE_SUFFIX; +import static org.cryptomator.cryptofs.common.Constants.INFLATED_FILE_NAME; @CryptoFileSystemScoped -class LongFileNameProvider { +public class LongFileNameProvider { private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name. private static final BaseEncoding BASE64 = BaseEncoding.base64Url(); diff --git a/src/main/java/org/cryptomator/cryptofs/RunnableThrowingException.java b/src/main/java/org/cryptomator/cryptofs/RunnableThrowingException.java deleted file mode 100644 index ab4c9e59..00000000 --- a/src/main/java/org/cryptomator/cryptofs/RunnableThrowingException.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.cryptofs; - -@FunctionalInterface -interface RunnableThrowingException { - - void run() throws E; - -} diff --git a/src/main/java/org/cryptomator/cryptofs/Symlinks.java b/src/main/java/org/cryptomator/cryptofs/Symlinks.java index 374fd7cf..ec198d11 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import javax.inject.Inject; diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java new file mode 100644 index 00000000..33b53f03 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2016, 2017 Sebastian Stenzel and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE.txt. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + *******************************************************************************/ +package org.cryptomator.cryptofs.common; + +public final class Constants { + + public static final int VAULT_VERSION = 7; + public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; + + public static final String DATA_DIR_NAME = "d"; + public static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 + public static final String ROOT_DIR_ID = ""; + public static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; + public static final String DEFLATED_FILE_SUFFIX = ".c9s"; + public static final String DIR_FILE_NAME = "dir.c9r"; + public static final String SYMLINK_FILE_NAME = "symlink.c9r"; + public static final String CONTENTS_FILE_NAME = "contents.c9r"; + public static final String INFLATED_FILE_NAME = "name.c9s"; + + public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 + + public static final String SEPARATOR = "/"; + +} diff --git a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java b/src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java similarity index 96% rename from src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java rename to src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java index bf0b107a..75ddde1a 100644 --- a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java +++ b/src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -63,7 +63,7 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx * @param path Path ot a single file or directory. Will not be deleted recursively. * @throws IOException exception thrown by delete. Any exceptions during removal of write protection will be ignored. */ - static void forceDeleteIfExists(Path path) throws IOException { + public static void forceDeleteIfExists(Path path) throws IOException { setWritableSilently(path); Files.deleteIfExists(path); } diff --git a/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java b/src/main/java/org/cryptomator/cryptofs/common/FinallyUtil.java similarity index 94% rename from src/main/java/org/cryptomator/cryptofs/FinallyUtil.java rename to src/main/java/org/cryptomator/cryptofs/common/FinallyUtil.java index 5651e330..d5ed16c4 100644 --- a/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java +++ b/src/main/java/org/cryptomator/cryptofs/common/FinallyUtil.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; import javax.inject.Inject; import javax.inject.Singleton; @@ -8,7 +8,7 @@ import java.util.stream.StreamSupport; @Singleton -class FinallyUtil { +public class FinallyUtil { @Inject public FinallyUtil() { diff --git a/src/main/java/org/cryptomator/cryptofs/common/RunnableThrowingException.java b/src/main/java/org/cryptomator/cryptofs/common/RunnableThrowingException.java new file mode 100644 index 00000000..9e348f42 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/common/RunnableThrowingException.java @@ -0,0 +1,8 @@ +package org.cryptomator.cryptofs.common; + +@FunctionalInterface +public interface RunnableThrowingException { + + void run() throws E; + +} diff --git a/src/main/java/org/cryptomator/cryptofs/StringUtils.java b/src/main/java/org/cryptomator/cryptofs/common/StringUtils.java similarity index 87% rename from src/main/java/org/cryptomator/cryptofs/StringUtils.java rename to src/main/java/org/cryptomator/cryptofs/common/StringUtils.java index 75c97b5e..f792c278 100644 --- a/src/main/java/org/cryptomator/cryptofs/StringUtils.java +++ b/src/main/java/org/cryptomator/cryptofs/common/StringUtils.java @@ -1,9 +1,9 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; /** * Functions used from commons-lang */ -final class StringUtils { +public final class StringUtils { public static String removeEnd(String str, String remove) { if (str == null || remove == null) { diff --git a/src/main/java/org/cryptomator/cryptofs/SupplierThrowingException.java b/src/main/java/org/cryptomator/cryptofs/common/SupplierThrowingException.java similarity index 73% rename from src/main/java/org/cryptomator/cryptofs/SupplierThrowingException.java rename to src/main/java/org/cryptomator/cryptofs/common/SupplierThrowingException.java index a5340a6c..fa5be758 100644 --- a/src/main/java/org/cryptomator/cryptofs/SupplierThrowingException.java +++ b/src/main/java/org/cryptomator/cryptofs/common/SupplierThrowingException.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; @FunctionalInterface public interface SupplierThrowingException { diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java similarity index 85% rename from src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java rename to src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java index 1ead9fc6..1efdcc4f 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java @@ -1,4 +1,8 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.CryptoFileSystemScoped; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.common.DeletingFileVisitor; import javax.inject.Inject; import java.io.IOException; @@ -9,11 +13,11 @@ import java.util.Set; import static java.util.stream.Collectors.toSet; -import static org.cryptomator.cryptofs.CiphertextDirectoryDeleter.DeleteResult.NO_FILES_DELETED; -import static org.cryptomator.cryptofs.CiphertextDirectoryDeleter.DeleteResult.SOME_FILES_DELETED; +import static org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter.DeleteResult.NO_FILES_DELETED; +import static org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter.DeleteResult.SOME_FILES_DELETED; @CryptoFileSystemScoped -class CiphertextDirectoryDeleter { +public class CiphertextDirectoryDeleter { private final DirectoryStreamFactory directoryStreamFactory; diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java similarity index 92% rename from src/main/java/org/cryptomator/cryptofs/ConflictResolver.java rename to src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java index 01d6f628..72e4555d 100644 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java @@ -1,8 +1,13 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import com.google.common.io.BaseEncoding; import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.CryptoFileSystemScoped; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.StringUtils; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; @@ -21,12 +26,11 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.cryptomator.cryptofs.Constants.CRYPTOMATOR_FILE_SUFFIX; -import static org.cryptomator.cryptofs.Constants.DEFLATED_FILE_SUFFIX; -import static org.cryptomator.cryptofs.Constants.DIR_FILE_NAME; -import static org.cryptomator.cryptofs.Constants.MAX_SYMLINK_LENGTH; -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; -import static org.cryptomator.cryptofs.Constants.SYMLINK_FILE_NAME; +import static org.cryptomator.cryptofs.common.Constants.CRYPTOMATOR_FILE_SUFFIX; +import static org.cryptomator.cryptofs.common.Constants.DEFLATED_FILE_SUFFIX; +import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME; +import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME; @CryptoFileSystemScoped class ConflictResolver { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java similarity index 94% rename from src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java rename to src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index d1d8d694..9885e439 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -6,10 +6,14 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.slf4j.Logger; diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java similarity index 85% rename from src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java rename to src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java index a9755c00..18e558ba 100644 --- a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -1,5 +1,15 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.CryptoFileSystemScoped; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.common.RunnableThrowingException; +import org.cryptomator.cryptolib.api.Cryptor; + +import javax.inject.Inject; import java.io.IOException; import java.nio.file.ClosedFileSystemException; import java.nio.file.DirectoryStream.Filter; @@ -7,13 +17,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.inject.Inject; - -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptolib.api.Cryptor; - @CryptoFileSystemScoped -class DirectoryStreamFactory { +public class DirectoryStreamFactory { private final Cryptor cryptor; private final LongFileNameProvider longFileNameProvider; diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java similarity index 94% rename from src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java rename to src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java index b0da9ba3..29acd1fb 100644 --- a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import javax.inject.Inject; import javax.inject.Singleton; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index 21072706..e8506250 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -15,7 +15,7 @@ import javax.inject.Inject; -import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java index dae0b087..20b43cf4 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java @@ -16,7 +16,7 @@ import javax.inject.Inject; import org.cryptomator.cryptofs.BackupUtil; -import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index cf5fcde7..57a0f0ec 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -6,8 +6,8 @@ package org.cryptomator.cryptofs.migration.v7; import org.cryptomator.cryptofs.BackupUtil; -import org.cryptomator.cryptofs.Constants; -import org.cryptomator.cryptofs.DeletingFileVisitor; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.DeletingFileVisitor; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 76927e87..3fc44d33 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -5,6 +5,10 @@ import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; import org.cryptomator.cryptofs.attr.AttributeViewType; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.common.RunnableThrowingException; +import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; +import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptofs.fh.OpenCryptoFiles.TwoPhaseMove; import org.cryptomator.cryptofs.mocks.FileChannelMock; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java index 829fec4d..fa7cbd71 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.DeletingFileVisitor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index d7aebaa9..f2d5d464 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.Constants; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,7 +26,7 @@ import java.util.stream.Stream; import static java.nio.file.StandardOpenOption.CREATE_NEW; -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.SHORT_NAMES_MAX_LENGTH; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; diff --git a/src/test/java/org/cryptomator/cryptofs/FinallyUtilTest.java b/src/test/java/org/cryptomator/cryptofs/common/FinallyUtilTest.java similarity index 94% rename from src/test/java/org/cryptomator/cryptofs/FinallyUtilTest.java rename to src/test/java/org/cryptomator/cryptofs/common/FinallyUtilTest.java index 4394784f..c4f325b4 100644 --- a/src/test/java/org/cryptomator/cryptofs/FinallyUtilTest.java +++ b/src/test/java/org/cryptomator/cryptofs/common/FinallyUtilTest.java @@ -1,5 +1,7 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.common.RunnableThrowingException; import org.junit.jupiter.api.Test; import org.mockito.InOrder; diff --git a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java similarity index 96% rename from src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java index fadd55d3..b8200092 100644 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java @@ -1,6 +1,10 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import com.google.common.base.Strings; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java similarity index 92% rename from src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index ac3556cd..aa9eec45 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -6,11 +6,12 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.CryptoDirectoryStream.ProcessedPaths; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.dir.CryptoDirectoryStream.ProcessedPaths; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.SHORT_NAMES_MAX_LENGTH; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java similarity index 93% rename from src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index 369c578f..9385515c 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -6,10 +6,16 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.common.RunnableThrowingException; +import org.cryptomator.cryptofs.common.StringUtils; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.CryptorProvider; diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java similarity index 90% rename from src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index 8a057568..225efb33 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -1,6 +1,14 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.FinallyUtil; +import org.cryptomator.cryptofs.common.RunnableThrowingException; +import org.cryptomator.cryptofs.dir.ConflictResolver; +import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; +import org.cryptomator.cryptofs.dir.EncryptedNamePattern; import org.cryptomator.cryptolib.api.Cryptor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java similarity index 90% rename from src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java index 0fd2371e..109d1f0f 100644 --- a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java @@ -1,5 +1,6 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.dir.EncryptedNamePattern; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java index 8552049d..16e77fd4 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java @@ -3,7 +3,7 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.BackupUtil; -import org.cryptomator.cryptofs.Constants; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; From dc34acd4a76c127e69c419c15db15345aa9b7c28 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 23 Oct 2019 10:13:19 +0200 Subject: [PATCH 42/62] Created new subcomponent for directory streams to enable DI for CryptoDirectoryStream --- .../cryptofs/CryptoFileSystemComponent.java | 5 +- .../cryptofs/CryptoFileSystemModule.java | 5 +- .../cryptofs/attr/AttributeViewProvider.java | 8 +- .../cryptofs/dir/ConflictResolver.java | 36 ++++++++ .../cryptofs/dir/CryptoDirectoryStream.java | 20 +++-- .../dir/DirectoryStreamComponent.java | 43 ++++++++++ .../cryptofs/dir/DirectoryStreamFactory.java | 84 ++++++++++--------- .../cryptofs/dir/DirectoryStreamScoped.java | 13 +++ .../cryptofs/fh/OpenCryptoFiles.java | 8 +- .../CryptoDirectoryStreamIntegrationTest.java | 4 +- .../dir/CryptoDirectoryStreamTest.java | 9 +- .../dir/DirectoryStreamFactoryTest.java | 69 ++++++--------- .../cryptofs/fh/OpenCryptoFilesTest.java | 5 +- 13 files changed, 195 insertions(+), 114 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java index 30903594..e1c0caa1 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java @@ -3,6 +3,7 @@ import dagger.BindsInstance; import dagger.Subcomponent; import org.cryptomator.cryptofs.attr.AttributeViewComponent; +import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; import java.nio.file.Path; @@ -13,10 +14,6 @@ public interface CryptoFileSystemComponent { CryptoFileSystemImpl cryptoFileSystem(); - OpenCryptoFileComponent.Builder newOpenCryptoFileComponent(); - - AttributeViewComponent.Builder newFileAttributeViewComponent(); - @Subcomponent.Builder interface Builder { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java index 92d2eb15..c95c03e8 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java @@ -7,7 +7,10 @@ import dagger.Module; import dagger.Provides; +import org.cryptomator.cryptofs.attr.AttributeViewComponent; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; +import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.KeyFile; @@ -19,7 +22,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -@Module +@Module(subcomponents = {AttributeViewComponent.class, OpenCryptoFileComponent.class, DirectoryStreamComponent.class}) class CryptoFileSystemModule { @Provides diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java index 9386e26f..28f07f72 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewProvider.java @@ -25,11 +25,11 @@ public class AttributeViewProvider { private static final Logger LOG = LoggerFactory.getLogger(AttributeViewProvider.class); - private final CryptoFileSystemComponent component; + private final AttributeViewComponent.Builder attrViewComponentBuilder; @Inject - AttributeViewProvider(CryptoFileSystemComponent component) { - this.component = component; + AttributeViewProvider(AttributeViewComponent.Builder attrViewComponentBuilder) { + this.attrViewComponentBuilder = attrViewComponentBuilder; } /** @@ -39,7 +39,7 @@ public class AttributeViewProvider { * @see Files#getFileAttributeView(java.nio.file.Path, Class, java.nio.file.LinkOption...) */ public A getAttributeView(CryptoPath cleartextPath, Class type, LinkOption... options) { - Optional view = component.newFileAttributeViewComponent() // + Optional view = attrViewComponentBuilder // .cleartextPath(cleartextPath) // .viewType(type) // .linkOptions(options) // diff --git a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java index 72e4555d..a0c582d3 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java @@ -87,6 +87,42 @@ public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throw } } + //private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}"); + //private static final Pattern DELIM_PATTERN = Pattern.compile("[!a-zA-Z0-9]"); +// public String extractCiphertext(String potentialCiphertext, byte[] dirIdBytes) { +// // attempt a full match: +// Matcher m = BASE64_PATTERN.matcher(potentialCiphertext); +// if (m.matches()) { +// try { +// String cleartext = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), potentialCiphertext, dirIdBytes); +// return potentialCiphertext; +// } catch (AuthenticationFailedException e) { +// // proceed with partial matches +// } +// } +// +// // strip potential prefix: +// Matcher d = DELIM_PATTERN.matcher(potentialCiphertext); +// if (d.find()) { +// int endOfPrefix = d.end(); +// assert endOfPrefix > 0; +// return extractCiphertext(potentialCiphertext.substring(endOfPrefix), dirIdBytes); +// } +// +// // strip potential suffix: +// int beginOfSuffix = 0; +// while (d.find(beginOfSuffix)) { // we can only loop through matches from begin to end to find the last match +// beginOfSuffix = d.start(); +// } +// if (beginOfSuffix > 0) { +// assert beginOfSuffix < potentialCiphertext.length() - 1; +// return extractCiphertext(potentialCiphertext.substring(0, beginOfSuffix), dirIdBytes); +// } +// +// // no match: +// return null; +// } + /** * Resolves a conflict. * diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index 9885e439..e4f3e8c1 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -9,16 +9,19 @@ package org.cryptomator.cryptofs.dir; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import javax.inject.Named; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryIteratorException; @@ -32,7 +35,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -class CryptoDirectoryStream implements DirectoryStream { +@DirectoryStreamScoped +public class CryptoDirectoryStream implements DirectoryStream { private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class); @@ -48,17 +52,17 @@ class CryptoDirectoryStream implements DirectoryStream { private final FinallyUtil finallyUtil; private final EncryptedNamePattern encryptedNamePattern; - public CryptoDirectoryStream(CiphertextDirectory ciphertextDir, Path cleartextDir, FileNameCryptor filenameCryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, - ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, FinallyUtil finallyUtil, EncryptedNamePattern encryptedNamePattern) - throws IOException { + @Inject + public CryptoDirectoryStream(CiphertextDirectory ciphertextDir, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, Cryptor cryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, + ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, FinallyUtil finallyUtil, EncryptedNamePattern encryptedNamePattern) { this.onClose = onClose; this.finallyUtil = finallyUtil; this.encryptedNamePattern = encryptedNamePattern; this.directoryId = ciphertextDir.dirId; - this.ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); + this.ciphertextDirStream = ciphertextDirStream; LOG.trace("OPEN {}", directoryId); this.cleartextDir = cleartextDir; - this.filenameCryptor = filenameCryptor; + this.filenameCryptor = cryptor.fileNameCryptor(); this.cryptoPathMapper = cryptoPathMapper; this.longFileNameProvider = longFileNameProvider; this.conflictResolver = conflictResolver; @@ -131,7 +135,7 @@ private ProcessedPaths decrypt(ProcessedPaths paths) { /** * Checks if a given file belongs into this ciphertext dir. - * + * * @param paths The path to check. * @return true if the file is an existing ciphertext or directory file. */ diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java new file mode 100644 index 00000000..6b353958 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java @@ -0,0 +1,43 @@ +package org.cryptomator.cryptofs.dir; + +import dagger.BindsInstance; +import dagger.Subcomponent; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; + +import javax.inject.Named; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.function.Consumer; + +@DirectoryStreamScoped +@Subcomponent +public interface DirectoryStreamComponent { + + CryptoDirectoryStream directoryStream(); + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + Builder cleartextPath(@Named("cleartextPath") Path cleartextPath); + + @BindsInstance + Builder ciphertextDirectory(CryptoPathMapper.CiphertextDirectory type); + + @BindsInstance + Builder ciphertextDirectoryStream(DirectoryStream ciphertextDirectoryStream); + + @BindsInstance + Builder filter(DirectoryStream.Filter filter); + + @BindsInstance + Builder onClose(Consumer onClose); + + DirectoryStreamComponent build(); + } + +} + + + diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java index 18e558ba..1afe5533 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -4,71 +4,73 @@ import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.LongFileNameProvider; import org.cryptomator.cryptofs.common.FinallyUtil; -import org.cryptomator.cryptofs.common.RunnableThrowingException; -import org.cryptomator.cryptolib.api.Cryptor; import javax.inject.Inject; import java.io.IOException; import java.nio.file.ClosedFileSystemException; +import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; @CryptoFileSystemScoped public class DirectoryStreamFactory { - - private final Cryptor cryptor; - private final LongFileNameProvider longFileNameProvider; - private final ConflictResolver conflictResolver; + private final CryptoPathMapper cryptoPathMapper; - private final FinallyUtil finallyUtil; - private final EncryptedNamePattern encryptedNamePattern; - - private final ConcurrentMap streams = new ConcurrentHashMap<>(); + private final DirectoryStreamComponent.Builder directoryStreamComponentBuilder; + private final Map streams = new HashMap<>(); private volatile boolean closed = false; @Inject - public DirectoryStreamFactory(Cryptor cryptor, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver, CryptoPathMapper cryptoPathMapper, FinallyUtil finallyUtil, - EncryptedNamePattern encryptedNamePattern) { - this.cryptor = cryptor; - this.longFileNameProvider = longFileNameProvider; - this.conflictResolver = conflictResolver; + public DirectoryStreamFactory(CryptoPathMapper cryptoPathMapper, DirectoryStreamComponent.Builder directoryStreamComponentBuilder) { this.cryptoPathMapper = cryptoPathMapper; - this.finallyUtil = finallyUtil; - this.encryptedNamePattern = encryptedNamePattern; + this.directoryStreamComponentBuilder = directoryStreamComponentBuilder; } - public CryptoDirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter filter) throws IOException { - CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); - CryptoDirectoryStream stream = new CryptoDirectoryStream( // - ciphertextDir, // - cleartextDir, // - cryptor.fileNameCryptor(), // - cryptoPathMapper, // - longFileNameProvider, // - conflictResolver, // - filter, // - closed -> streams.remove(closed), // - finallyUtil, // - encryptedNamePattern); - streams.put(stream, stream); + public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter filter) throws IOException { if (closed) { - stream.close(); throw new ClosedFileSystemException(); } - return stream; + CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); + DirectoryStream ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); + CryptoDirectoryStream cleartextDirStream = directoryStreamComponentBuilder // + .ciphertextDirectory(ciphertextDir) // + .ciphertextDirectoryStream(ciphertextDirStream) // + .cleartextPath(cleartextDir) // + .filter(filter) // + .onClose(streams::remove) // + .build() // + .directoryStream(); + streams.put(cleartextDirStream, ciphertextDirStream); + return cleartextDirStream; } - public void close() throws IOException { + public synchronized void close() throws IOException { closed = true; - finallyUtil.guaranteeInvocationOf( // - streams.keySet().stream() // - .map(stream -> (RunnableThrowingException) () -> stream.close()) // - .iterator()); + IOException exception = new IOException("Close failed"); + Iterator> iter = streams.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + iter.remove(); + try { + entry.getKey().close(); + } catch (IOException e) { + exception.addSuppressed(e); + } + try { + entry.getValue().close(); + } catch (IOException e) { + exception.addSuppressed(e); + } + } + if (exception.getSuppressed().length > 0) { + throw exception; + } } } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java new file mode 100644 index 00000000..a7103d27 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.cryptofs.dir; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Scope +@Documented +@Retention(RUNTIME) +public @interface DirectoryStreamScoped { +} diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java index 02018756..dea5f027 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java @@ -32,12 +32,12 @@ @CryptoFileSystemScoped public class OpenCryptoFiles implements Closeable { - private final CryptoFileSystemComponent component; + private final OpenCryptoFileComponent.Builder openCryptoFileComponentBuilder; private final ConcurrentMap openCryptoFiles = new ConcurrentHashMap<>(); @Inject - OpenCryptoFiles(CryptoFileSystemComponent component) { - this.component = component; + OpenCryptoFiles(OpenCryptoFileComponent.Builder openCryptoFileComponentBuilder) { + this.openCryptoFileComponentBuilder = openCryptoFileComponentBuilder; } /** @@ -67,7 +67,7 @@ public OpenCryptoFile getOrCreate(Path ciphertextPath) { } private OpenCryptoFile create(Path normalizedPath) { - OpenCryptoFileComponent openCryptoFileComponent = component.newOpenCryptoFileComponent() + OpenCryptoFileComponent openCryptoFileComponent = openCryptoFileComponentBuilder .path(normalizedPath) .onClose(openCryptoFiles::remove) .build(); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index aa9eec45..b62e6bc5 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -12,9 +12,11 @@ import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.LongFileNameProvider; import org.cryptomator.cryptofs.dir.CryptoDirectoryStream.ProcessedPaths; +import org.cryptomator.cryptolib.api.Cryptor; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; import java.nio.file.FileSystem; @@ -43,7 +45,7 @@ public void setup() throws IOException { Path dir = fileSystem.getPath("crapDirDoNotUse"); Files.createDirectory(dir); - inTest = new CryptoDirectoryStream(new CiphertextDirectory("", dir), null, null, null, longFileNameProvider, null, null, null, null, null); + inTest = new CryptoDirectoryStream(new CiphertextDirectory("", dir), null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null, null); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index 9385515c..28ff9424 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -18,6 +18,7 @@ import org.cryptomator.cryptofs.common.StringUtils; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; +import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; @@ -52,6 +53,7 @@ public class CryptoDirectoryStreamTest { private static final Filter ACCEPT_ALL = ignored -> true; private static CryptorProvider CRYPTOR_PROVIDER = Cryptors.version1(NullSecureRandom.INSTANCE); + private Cryptor cryptor; private FileNameCryptor filenameCryptor; private Path ciphertextDirPath; private DirectoryStream dirStream; @@ -64,7 +66,8 @@ public class CryptoDirectoryStreamTest { @BeforeEach @SuppressWarnings("unchecked") public void setup() throws IOException { - filenameCryptor = CRYPTOR_PROVIDER.createNew().fileNameCryptor(); + cryptor = CRYPTOR_PROVIDER.createNew(); + filenameCryptor = cryptor.fileNameCryptor(); ciphertextDirPath = Mockito.mock(Path.class); FileSystem fs = Mockito.mock(FileSystem.class); @@ -124,7 +127,7 @@ public void testDirListing() throws IOException { ciphertextFileNames.add("alsoInvalid"); Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator()); - try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), cleartextPath, filenameCryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, + try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertTrue(iter.hasNext()); @@ -147,7 +150,7 @@ public void testDirListingForEmptyDir() throws IOException { Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); - try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), cleartextPath, filenameCryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, + try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertFalse(iter.hasNext()); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index 225efb33..dfba393c 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -3,16 +3,10 @@ import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.FinallyUtil; -import org.cryptomator.cryptofs.common.RunnableThrowingException; -import org.cryptomator.cryptofs.dir.ConflictResolver; -import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; -import org.cryptomator.cryptofs.dir.EncryptedNamePattern; -import org.cryptomator.cryptolib.api.Cryptor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import java.io.IOException; import java.nio.file.ClosedFileSystemException; @@ -21,46 +15,35 @@ import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.spi.FileSystemProvider; -import java.util.Iterator; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class DirectoryStreamFactoryTest { - private final FileSystem fileSystem = mock(FileSystem.class); - private final FileSystemProvider provider = mock(FileSystemProvider.class); - private final FinallyUtil finallyUtil = mock(FinallyUtil.class); - private final Cryptor cryptor = mock(Cryptor.class); - private final LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class); - private final ConflictResolver conflictResolver = mock(ConflictResolver.class); + private final FileSystem fileSystem = mock(FileSystem.class, "fs"); + private final FileSystemProvider provider = mock(FileSystemProvider.class, "provider"); private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class); - private final EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern(); + private final DirectoryStreamComponent directoryStreamComp = mock(DirectoryStreamComponent.class); + private final DirectoryStreamComponent.Builder directoryStreamBuilder = mock(DirectoryStreamComponent.Builder.class); - private final DirectoryStreamFactory inTest = new DirectoryStreamFactory(cryptor, longFileNameProvider, conflictResolver, cryptoPathMapper, finallyUtil, encryptedNamePattern); + private final DirectoryStreamFactory inTest = new DirectoryStreamFactory(cryptoPathMapper, directoryStreamBuilder); @SuppressWarnings("unchecked") @BeforeEach - public void setup() { - doAnswer(invocation -> { - for (Object runnable : invocation.getArguments()) { - ((RunnableThrowingException) runnable).run(); - } - return null; - }).when(finallyUtil).guaranteeInvocationOf(any(RunnableThrowingException.class), any(RunnableThrowingException.class), any(RunnableThrowingException.class)); - doAnswer(invocation -> { - Iterator> iterator = invocation.getArgument(0); - while (iterator.hasNext()) { - iterator.next().run(); - } - return null; - }).when(finallyUtil).guaranteeInvocationOf(any(Iterator.class)); + public void setup() throws IOException { + when(directoryStreamBuilder.cleartextPath(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.ciphertextDirectory(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.ciphertextDirectoryStream(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.filter(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.onClose(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.build()).thenReturn(directoryStreamComp); + when(directoryStreamComp.directoryStream()).then(invocation -> mock(CryptoDirectoryStream.class)); when(fileSystem.provider()).thenReturn(provider); } @@ -70,9 +53,11 @@ public void testNewDirectoryStreamCreatesDirectoryStream() throws IOException { CryptoPath path = mock(CryptoPath.class); Filter filter = mock(Filter.class); String dirId = "dirIdAbc"; - Path dirPath = mock(Path.class); + Path dirPath = mock(Path.class, "dirAbc"); when(dirPath.getFileSystem()).thenReturn(fileSystem); when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, dirPath)); + DirectoryStream stream = mock(DirectoryStream.class); + when(provider.newDirectoryStream(same(dirPath), any())).thenReturn(stream); DirectoryStream directoryStream = inTest.newDirectoryStream(path, filter); @@ -83,16 +68,16 @@ public void testNewDirectoryStreamCreatesDirectoryStream() throws IOException { @Test public void testCloseClosesAllNonClosedDirectoryStreams() throws IOException { Filter filter = mock(Filter.class); - CryptoPath pathA = mock(CryptoPath.class); - CryptoPath pathB = mock(CryptoPath.class); - Path dirPathA = mock(Path.class); + CryptoPath pathA = mock(CryptoPath.class, "pathA"); + CryptoPath pathB = mock(CryptoPath.class, "pathB"); + Path dirPathA = mock(Path.class, "dirPathA"); when(dirPathA.getFileSystem()).thenReturn(fileSystem); - Path dirPathB = mock(Path.class); + Path dirPathB = mock(Path.class, "dirPathB"); when(dirPathB.getFileSystem()).thenReturn(fileSystem); when(cryptoPathMapper.getCiphertextDir(pathA)).thenReturn(new CiphertextDirectory("dirIdA", dirPathA)); when(cryptoPathMapper.getCiphertextDir(pathB)).thenReturn(new CiphertextDirectory("dirIdB", dirPathB)); - DirectoryStream streamA = mock(DirectoryStream.class); - DirectoryStream streamB = mock(DirectoryStream.class); + DirectoryStream streamA = mock(DirectoryStream.class, "streamA"); + DirectoryStream streamB = mock(DirectoryStream.class, "streamB"); when(provider.newDirectoryStream(same(dirPathA), any())).thenReturn(streamA); when(provider.newDirectoryStream(same(dirPathB), any())).thenReturn(streamB); @@ -110,13 +95,9 @@ public void testCloseClosesAllNonClosedDirectoryStreams() throws IOException { public void testNewDirectoryStreamAfterClosedThrowsClosedFileSystemException() throws IOException { CryptoPath path = mock(CryptoPath.class); Filter filter = mock(Filter.class); - String dirId = "dirIdAbc"; - Path dirPath = mock(Path.class); - when(dirPath.getFileSystem()).thenReturn(fileSystem); - when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, dirPath)); - when(provider.newDirectoryStream(same(dirPath), any())).thenReturn(mock(DirectoryStream.class)); - + inTest.close(); + Assertions.assertThrows(ClosedFileSystemException.class, () -> { inTest.newDirectoryStream(path, filter); }); diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java index 673fe75e..f043e701 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java @@ -25,7 +25,6 @@ public class OpenCryptoFilesTest { - private final CryptoFileSystemComponent cryptoFileSystemComponent = mock(CryptoFileSystemComponent.class); private final OpenCryptoFileComponent.Builder openCryptoFileComponentBuilder = mock(OpenCryptoFileComponent.Builder.class); private final OpenCryptoFile file = mock(OpenCryptoFile.class, "file"); private final FileChannel ciphertextFileChannel = Mockito.mock(FileChannel.class); @@ -37,7 +36,6 @@ public void setup() throws IOException, ReflectiveOperationException { OpenCryptoFileComponent subComponent = mock(OpenCryptoFileComponent.class); Mockito.when(subComponent.openCryptoFile()).thenReturn(file); - Mockito.when(cryptoFileSystemComponent.newOpenCryptoFileComponent()).thenReturn(openCryptoFileComponentBuilder); Mockito.when(openCryptoFileComponentBuilder.path(Mockito.any())).thenReturn(openCryptoFileComponentBuilder); Mockito.when(openCryptoFileComponentBuilder.onClose(Mockito.any())).thenReturn(openCryptoFileComponentBuilder); Mockito.when(openCryptoFileComponentBuilder.build()).thenReturn(subComponent); @@ -47,7 +45,7 @@ public void setup() throws IOException, ReflectiveOperationException { closeLockField.setAccessible(true); closeLockField.set(ciphertextFileChannel, new Object()); - inTest = new OpenCryptoFiles(cryptoFileSystemComponent); + inTest = new OpenCryptoFiles(openCryptoFileComponentBuilder); } @Test @@ -60,7 +58,6 @@ public void testGetOrCreate() { OpenCryptoFile file2 = mock(OpenCryptoFile.class); Mockito.when(subComponent2.openCryptoFile()).thenReturn(file2); - Mockito.when(cryptoFileSystemComponent.newOpenCryptoFileComponent()).thenReturn(openCryptoFileComponentBuilder); Mockito.when(openCryptoFileComponentBuilder.build()).thenReturn(subComponent1, subComponent2); Path p1 = Paths.get("/foo"); From ebfe5eae89a39997f857307a4a4b7bb3673df065 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 23 Oct 2019 15:46:52 +0200 Subject: [PATCH 43/62] repackaged some more classes --- .../org/cryptomator/cryptofs/CopyOperation.java | 2 ++ .../cryptofs/CryptoFileSystemImpl.java | 2 ++ .../cryptofs/CryptoFileSystemProvider.java | 1 + .../org/cryptomator/cryptofs/CryptoPath.java | 2 ++ .../cryptomator/cryptofs/CryptoPathMapper.java | 1 + .../org/cryptomator/cryptofs/MoveOperation.java | 2 ++ .../java/org/cryptomator/cryptofs/Symlinks.java | 1 + .../attr/AbstractCryptoFileAttributeView.java | 4 ++-- .../cryptofs/attr/AttributeProvider.java | 4 ++-- .../cryptofs/attr/CryptoBasicFileAttributes.java | 2 +- .../cryptofs/attr/CryptoDosFileAttributes.java | 2 +- .../cryptofs/attr/CryptoPosixFileAttributes.java | 2 +- .../{ => ch}/AsyncDelegatingFileChannel.java | 16 +++++----------- .../cryptofs/{ => common}/ArrayUtils.java | 2 +- .../{ => common}/CiphertextFileType.java | 2 +- .../cryptofs/CryptoFileSystemImplTest.java | 1 + .../cryptofs/CryptoFileSystemProviderTest.java | 1 + .../cryptofs/CryptoPathMapperTest.java | 1 + .../org/cryptomator/cryptofs/SymlinksTest.java | 1 + .../cryptofs/attr/AttributeProviderTest.java | 2 +- .../attr/CryptoBasicFileAttributesTest.java | 6 +++--- .../attr/CryptoDosFileAttributeViewTest.java | 2 +- .../attr/CryptoDosFileAttributesTest.java | 2 +- .../attr/CryptoFileOwnerAttributeViewTest.java | 2 +- .../attr/CryptoPosixFileAttributeViewTest.java | 2 +- .../attr/CryptoPosixFileAttributesTest.java | 2 +- .../{ => ch}/AsyncDelegatingFileChannelTest.java | 3 ++- 27 files changed, 40 insertions(+), 30 deletions(-) rename src/main/java/org/cryptomator/cryptofs/{ => ch}/AsyncDelegatingFileChannel.java (89%) rename src/main/java/org/cryptomator/cryptofs/{ => common}/ArrayUtils.java (94%) rename src/main/java/org/cryptomator/cryptofs/{ => common}/CiphertextFileType.java (81%) rename src/test/java/org/cryptomator/cryptofs/{ => ch}/AsyncDelegatingFileChannelTest.java (96%) diff --git a/src/main/java/org/cryptomator/cryptofs/CopyOperation.java b/src/main/java/org/cryptomator/cryptofs/CopyOperation.java index 1cddd564..dda03760 100644 --- a/src/main/java/org/cryptomator/cryptofs/CopyOperation.java +++ b/src/main/java/org/cryptomator/cryptofs/CopyOperation.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.ArrayUtils; + import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index ea12b6d3..b846b79a 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -13,6 +13,8 @@ import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; import org.cryptomator.cryptofs.attr.AttributeViewType; +import org.cryptomator.cryptofs.common.ArrayUtils; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.common.DeletingFileVisitor; import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 3b8cd20f..f0fb1415 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptolib.Cryptors; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java index 7864cb72..044a0cdc 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.ArrayUtils; + import java.io.File; import java.io.IOException; import java.net.URI; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 8546b9f5..707bc796 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -14,6 +14,7 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/MoveOperation.java b/src/main/java/org/cryptomator/cryptofs/MoveOperation.java index 3716eddd..c19add08 100644 --- a/src/main/java/org/cryptomator/cryptofs/MoveOperation.java +++ b/src/main/java/org/cryptomator/cryptofs/MoveOperation.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.ArrayUtils; + import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; diff --git a/src/main/java/org/cryptomator/cryptofs/Symlinks.java b/src/main/java/org/cryptomator/cryptofs/Symlinks.java index ec198d11..1084e6c2 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 0fd693bc..6e45aeaf 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -8,8 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.ArrayUtils; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.ArrayUtils; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java index f67d8e24..6146bc62 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java @@ -8,8 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.ArrayUtils; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.ArrayUtils; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java index de7828d2..920f7a19 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java @@ -8,7 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributes.java index 4d2e4a94..74525a03 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributes.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributes.java @@ -12,7 +12,7 @@ import java.nio.file.attribute.DosFileAttributes; import java.util.Optional; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributes.java index a8fd58fe..88de551c 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributes.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributes.java @@ -9,7 +9,7 @@ package org.cryptomator.cryptofs.attr; import com.google.common.collect.Sets; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptolib.api.Cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java similarity index 89% rename from src/main/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannel.java rename to src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java index 5f3f3d2b..9cd5ae2e 100644 --- a/src/main/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannel.java +++ b/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.ch; import java.io.IOException; import java.nio.ByteBuffer; @@ -22,7 +22,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -class AsyncDelegatingFileChannel extends AsynchronousFileChannel { +public class AsyncDelegatingFileChannel extends AsynchronousFileChannel { private final FileChannel channel; private final ExecutorService executor; @@ -84,9 +84,7 @@ public Future lock(long position, long size, boolean shared) { if (!isOpen()) { return exceptionalFuture(new ClosedChannelException()); } - return executor.submit(() -> { - return channel.lock(position, size, shared); - }); + return executor.submit(() -> channel.lock(position, size, shared)); } @Override @@ -104,9 +102,7 @@ public Future read(ByteBuffer dst, long position) { if (!isOpen()) { return exceptionalFuture(new ClosedChannelException()); } - return executor.submit(() -> { - return channel.read(dst, position); - }); + return executor.submit(() -> channel.read(dst, position)); } @Override @@ -119,9 +115,7 @@ public Future write(ByteBuffer src, long position) { if (!isOpen()) { return exceptionalFuture(new ClosedChannelException()); } - return executor.submit(() -> { - return channel.write(src, position); - }); + return executor.submit(() -> channel.write(src, position)); } private Future exceptionalFuture(Throwable exception) { diff --git a/src/main/java/org/cryptomator/cryptofs/ArrayUtils.java b/src/main/java/org/cryptomator/cryptofs/common/ArrayUtils.java similarity index 94% rename from src/main/java/org/cryptomator/cryptofs/ArrayUtils.java rename to src/main/java/org/cryptomator/cryptofs/common/ArrayUtils.java index 2706c661..36f81199 100644 --- a/src/main/java/org/cryptomator/cryptofs/ArrayUtils.java +++ b/src/main/java/org/cryptomator/cryptofs/common/ArrayUtils.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; import java.util.Arrays; import java.util.Objects; diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java b/src/main/java/org/cryptomator/cryptofs/common/CiphertextFileType.java similarity index 81% rename from src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java rename to src/main/java/org/cryptomator/cryptofs/common/CiphertextFileType.java index f4a034b8..71fb24b5 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java +++ b/src/main/java/org/cryptomator/cryptofs/common/CiphertextFileType.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; /** * Filename prefix as defined issue 38. diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 3fc44d33..608a3eb6 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -5,6 +5,7 @@ import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; import org.cryptomator.cryptofs.attr.AttributeViewType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptofs.common.RunnableThrowingException; import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index 0bca259b..4fc54446 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java @@ -3,6 +3,7 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; +import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 7424b3a3..f1771b24 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java index 2c95be75..0fa01436 100644 --- a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java +++ b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java index 4fab54a6..f9269de1 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java @@ -9,7 +9,7 @@ package org.cryptomator.cryptofs.attr; import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java index 73cca624..953a4ac7 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java @@ -20,9 +20,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.Optional; -import static org.cryptomator.cryptofs.CiphertextFileType.DIRECTORY; -import static org.cryptomator.cryptofs.CiphertextFileType.FILE; -import static org.cryptomator.cryptofs.CiphertextFileType.SYMLINK; +import static org.cryptomator.cryptofs.common.CiphertextFileType.DIRECTORY; +import static org.cryptomator.cryptofs.common.CiphertextFileType.FILE; +import static org.cryptomator.cryptofs.common.CiphertextFileType.SYMLINK; public class CryptoBasicFileAttributesTest { diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java index 2d53ad8f..82852965 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java @@ -1,7 +1,7 @@ package org.cryptomator.cryptofs.attr; import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java index 39398d01..6cfee36a 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java @@ -16,7 +16,7 @@ import java.nio.file.attribute.DosFileAttributes; import java.util.Optional; -import static org.cryptomator.cryptofs.CiphertextFileType.FILE; +import static org.cryptomator.cryptofs.common.CiphertextFileType.FILE; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java index 87203417..1ad616ad 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -1,7 +1,7 @@ package org.cryptomator.cryptofs.attr; import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java index 6806f53a..8a6e9e89 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java @@ -1,7 +1,7 @@ package org.cryptomator.cryptofs.attr; import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributesTest.java index 9b869f63..41d12f25 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributesTest.java @@ -21,7 +21,7 @@ import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static org.cryptomator.cryptofs.CiphertextFileType.FILE; +import static org.cryptomator.cryptofs.common.CiphertextFileType.FILE; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java similarity index 96% rename from src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java rename to src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java index 3901b704..ce452646 100644 --- a/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java @@ -6,8 +6,9 @@ * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.ch; +import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; From 7bffc619dced9a524cee1ce292b10be6fae19d07 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 23 Oct 2019 15:55:08 +0200 Subject: [PATCH 44/62] fixed test --- .../cryptofs/ch/AsyncDelegatingFileChannel.java | 16 ---------------- .../cryptofs/CryptoFileSystemProviderTest.java | 10 +++------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java index 9cd5ae2e..b71c1ec0 100644 --- a/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java +++ b/src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java @@ -32,22 +32,6 @@ public AsyncDelegatingFileChannel(FileChannel channel, ExecutorService executor) this.executor = executor; } - /** - * @deprecated only for testing - */ - @Deprecated - FileChannel getChannel() { - return channel; - } - - /** - * @deprecated only for testing - */ - @Deprecated - ExecutorService getExecutor() { - return executor; - } - @Override public void close() throws IOException { channel.close(); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index 4fc54446..4ba4731e 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java @@ -362,8 +362,7 @@ public void testNewAsyncFileChannelFailsIfOptionsContainAppend() { } @Test - @SuppressWarnings("deprecation") - public void testNewAsyncFileChannelReturnsAsyncDelegatingFileChannelWithNewFileChannelAndExecutor() throws IOException { + public void testNewAsyncFileChannelReturnsAsyncDelegatingFileChannel() throws IOException { @SuppressWarnings("unchecked") Set options = mock(Set.class); ExecutorService executor = mock(ExecutorService.class); @@ -371,11 +370,8 @@ public void testNewAsyncFileChannelReturnsAsyncDelegatingFileChannelWithNewFileC when(cryptoFileSystem.newFileChannel(cryptoPath, options)).thenReturn(channel); AsynchronousFileChannel result = inTest.newAsynchronousFileChannel(cryptoPath, options, executor); - - MatcherAssert.assertThat(result, is(instanceOf(AsyncDelegatingFileChannel.class))); - AsyncDelegatingFileChannel asyncDelegatingFileChannel = (AsyncDelegatingFileChannel) result; - Assertions.assertSame(channel, asyncDelegatingFileChannel.getChannel()); - Assertions.assertSame(executor, asyncDelegatingFileChannel.getExecutor()); + + MatcherAssert.assertThat(result, instanceOf(AsyncDelegatingFileChannel.class)); } @Test From 4ee52cd921228eacb57f6f9ce32fb2309c892438 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 23 Oct 2019 15:55:27 +0200 Subject: [PATCH 45/62] removed finallyutil from dirstream --- .../cryptofs/dir/CryptoDirectoryStream.java | 16 +++++++-------- .../CryptoDirectoryStreamIntegrationTest.java | 2 +- .../dir/CryptoDirectoryStreamTest.java | 20 +++---------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index e4f3e8c1..d8965913 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -13,7 +13,6 @@ import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.LongFileNameProvider; import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptofs.common.FinallyUtil; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileNameCryptor; @@ -49,14 +48,12 @@ public class CryptoDirectoryStream implements DirectoryStream { private final ConflictResolver conflictResolver; private final DirectoryStream.Filter filter; private final Consumer onClose; - private final FinallyUtil finallyUtil; private final EncryptedNamePattern encryptedNamePattern; @Inject public CryptoDirectoryStream(CiphertextDirectory ciphertextDir, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, Cryptor cryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, - ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, FinallyUtil finallyUtil, EncryptedNamePattern encryptedNamePattern) { + ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, EncryptedNamePattern encryptedNamePattern) { this.onClose = onClose; - this.finallyUtil = finallyUtil; this.encryptedNamePattern = encryptedNamePattern; this.directoryId = ciphertextDir.dirId; this.ciphertextDirStream = ciphertextDirStream; @@ -175,12 +172,13 @@ private boolean isAcceptableByFilter(Path path) { } @Override - @SuppressWarnings("unchecked") public void close() throws IOException { - finallyUtil.guaranteeInvocationOf( // - () -> ciphertextDirStream.close(), // - () -> onClose.accept(this), // - () -> LOG.trace("CLOSE {}", directoryId)); + try { + ciphertextDirStream.close(); + LOG.trace("CLOSE {}", directoryId); + } finally { + onClose.accept(this); + } } static class ProcessedPaths { diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index b62e6bc5..03ab9f76 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -45,7 +45,7 @@ public void setup() throws IOException { Path dir = fileSystem.getPath("crapDirDoNotUse"); Files.createDirectory(dir); - inTest = new CryptoDirectoryStream(new CiphertextDirectory("", dir), null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null, null); + inTest = new CryptoDirectoryStream(new CiphertextDirectory("", dir), null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index 28ff9424..292e12e1 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -9,12 +9,10 @@ package org.cryptomator.cryptofs.dir; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.FinallyUtil; -import org.cryptomator.cryptofs.common.RunnableThrowingException; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.StringUtils; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; @@ -42,9 +40,6 @@ import java.util.function.Consumer; import static org.mockito.AdditionalAnswers.returnsFirstArg; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; public class CryptoDirectoryStreamTest { @@ -60,7 +55,6 @@ public class CryptoDirectoryStreamTest { private CryptoPathMapper cryptoPathMapper; private LongFileNameProvider longFileNameProvider; private ConflictResolver conflictResolver; - private FinallyUtil finallyUtil; private EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern(); @BeforeEach @@ -78,7 +72,6 @@ public void setup() throws IOException { Mockito.when(provider.newDirectoryStream(Mockito.same(ciphertextDirPath), Mockito.any())).thenReturn(dirStream); longFileNameProvider = Mockito.mock(LongFileNameProvider.class); conflictResolver = Mockito.mock(ConflictResolver.class); - finallyUtil = mock(FinallyUtil.class); Mockito.when(longFileNameProvider.inflate(Mockito.any())).then(invocation -> { String shortName = invocation.getArgument(0); if (shortName.contains("invalid")) { @@ -102,13 +95,6 @@ public void setup() throws IOException { }); Mockito.when(conflictResolver.resolveConflictsIfNecessary(Mockito.any(), Mockito.any())).then(returnsFirstArg()); - - doAnswer(invocation -> { - for (Object runnable : invocation.getArguments()) { - ((RunnableThrowingException) runnable).run(); - } - return null; - }).when(finallyUtil).guaranteeInvocationOf(any(RunnableThrowingException.class), any(RunnableThrowingException.class), any(RunnableThrowingException.class)); } @Test @@ -128,7 +114,7 @@ public void testDirListing() throws IOException { Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator()); try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, - DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { + DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertTrue(iter.hasNext()); Assertions.assertEquals(cleartextPath.resolve("one"), iter.next()); @@ -151,7 +137,7 @@ public void testDirListingForEmptyDir() throws IOException { Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, - DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { + DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertFalse(iter.hasNext()); Assertions.assertThrows(NoSuchElementException.class, () -> { From 148d8d26684b55b6c5adef53b13fa457a2a1e279 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 23 Oct 2019 18:08:33 +0200 Subject: [PATCH 46/62] extracted NodeNames (formerly known as ProcessedPaths) --- .../cryptofs/dir/CryptoDirectoryStream.java | 74 ++++--------------- .../dir/DirectoryStreamComponent.java | 2 +- .../cryptofs/dir/DirectoryStreamFactory.java | 3 +- .../cryptomator/cryptofs/dir/NodeNames.java | 45 +++++++++++ .../CryptoDirectoryStreamIntegrationTest.java | 42 ++++------- .../dir/CryptoDirectoryStreamTest.java | 16 ++-- .../dir/DirectoryStreamFactoryTest.java | 2 +- 7 files changed, 82 insertions(+), 102 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index d8965913..c9837f95 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -51,13 +51,13 @@ public class CryptoDirectoryStream implements DirectoryStream { private final EncryptedNamePattern encryptedNamePattern; @Inject - public CryptoDirectoryStream(CiphertextDirectory ciphertextDir, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, Cryptor cryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, + public CryptoDirectoryStream(@Named("dirId") String dirId, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, Cryptor cryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, EncryptedNamePattern encryptedNamePattern) { + LOG.trace("OPEN {}", dirId); this.onClose = onClose; this.encryptedNamePattern = encryptedNamePattern; - this.directoryId = ciphertextDir.dirId; + this.directoryId = dirId; this.ciphertextDirStream = ciphertextDirStream; - LOG.trace("OPEN {}", directoryId); this.cleartextDir = cleartextDir; this.filenameCryptor = cryptor.fileNameCryptor(); this.cryptoPathMapper = cryptoPathMapper; @@ -73,24 +73,24 @@ public Iterator iterator() { private Stream cleartextDirectoryListing() { return directoryListing() // - .map(ProcessedPaths::getCleartextPath) // + .map(NodeNames::getCleartextPath) // .filter(this::isAcceptableByFilter); } public Stream ciphertextDirectoryListing() { - return directoryListing().map(ProcessedPaths::getCiphertextPath); + return directoryListing().map(NodeNames::getCiphertextPath); } - private Stream directoryListing() { - Stream pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(ProcessedPaths::new); - Stream resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull); - Stream inflated = resolved.map(this::inflateIfNeeded).filter(Objects::nonNull); - Stream decrypted = inflated.map(this::decrypt).filter(Objects::nonNull); - Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); + private Stream directoryListing() { + Stream pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(NodeNames::new); + Stream resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull); + Stream inflated = resolved.map(this::inflateIfNeeded).filter(Objects::nonNull); + Stream decrypted = inflated.map(this::decrypt).filter(Objects::nonNull); + Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); return sanitized; } - private ProcessedPaths resolveConflictingFileIfNeeded(ProcessedPaths paths) { + private NodeNames resolveConflictingFileIfNeeded(NodeNames paths) { try { return paths.withCiphertextPath(conflictResolver.resolveConflictsIfNecessary(paths.getCiphertextPath(), directoryId)); } catch (IOException e) { @@ -99,7 +99,7 @@ private ProcessedPaths resolveConflictingFileIfNeeded(ProcessedPaths paths) { } } - ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { + NodeNames inflateIfNeeded(NodeNames paths) { String fileName = paths.getCiphertextPath().getFileName().toString(); if (longFileNameProvider.isDeflated(fileName)) { try { @@ -114,7 +114,7 @@ ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { } } - private ProcessedPaths decrypt(ProcessedPaths paths) { + private NodeNames decrypt(NodeNames paths) { Optional ciphertextName = encryptedNamePattern.extractEncryptedName(paths.getInflatedPath()); if (ciphertextName.isPresent()) { String ciphertext = ciphertextName.get(); @@ -136,11 +136,11 @@ private ProcessedPaths decrypt(ProcessedPaths paths) { * @param paths The path to check. * @return true if the file is an existing ciphertext or directory file. */ - private boolean passesPlausibilityChecks(ProcessedPaths paths) { + private boolean passesPlausibilityChecks(NodeNames paths) { return !isBrokenDirectoryFile(paths); } - private boolean isBrokenDirectoryFile(ProcessedPaths paths) { + private boolean isBrokenDirectoryFile(NodeNames paths) { Path potentialDirectoryFile = paths.getCiphertextPath().resolve(Constants.DIR_FILE_NAME); if (Files.isRegularFile(potentialDirectoryFile)) { final Path dirPath; @@ -181,46 +181,4 @@ public void close() throws IOException { } } - static class ProcessedPaths { - - private final Path ciphertextPath; - private final Path inflatedPath; - private final Path cleartextPath; - - public ProcessedPaths(Path ciphertextPath) { - this(ciphertextPath, null, null); - } - - private ProcessedPaths(Path ciphertextPath, Path inflatedPath, Path cleartextPath) { - this.ciphertextPath = ciphertextPath; - this.inflatedPath = inflatedPath; - this.cleartextPath = cleartextPath; - } - - public Path getCiphertextPath() { - return ciphertextPath; - } - - public Path getInflatedPath() { - return inflatedPath; - } - - public Path getCleartextPath() { - return cleartextPath; - } - - public ProcessedPaths withCiphertextPath(Path ciphertextPath) { - return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath); - } - - public ProcessedPaths withInflatedPath(Path inflatedPath) { - return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath); - } - - public ProcessedPaths withCleartextPath(Path cleartextPath) { - return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath); - } - - } - } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java index 6b353958..800b321c 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamComponent.java @@ -23,7 +23,7 @@ interface Builder { Builder cleartextPath(@Named("cleartextPath") Path cleartextPath); @BindsInstance - Builder ciphertextDirectory(CryptoPathMapper.CiphertextDirectory type); + Builder dirId(@Named("dirId") String dirId); @BindsInstance Builder ciphertextDirectoryStream(DirectoryStream ciphertextDirectoryStream); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java index 1afe5533..5a5556d7 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -4,7 +4,6 @@ import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.common.FinallyUtil; import javax.inject.Inject; import java.io.IOException; @@ -39,7 +38,7 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); DirectoryStream ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); CryptoDirectoryStream cleartextDirStream = directoryStreamComponentBuilder // - .ciphertextDirectory(ciphertextDir) // + .dirId(ciphertextDir.dirId) // .ciphertextDirectoryStream(ciphertextDirStream) // .cleartextPath(cleartextDir) // .filter(filter) // diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java new file mode 100644 index 00000000..ae5e0bfe --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java @@ -0,0 +1,45 @@ +package org.cryptomator.cryptofs.dir; + +import java.nio.file.Path; + +class NodeNames { + + private final Path ciphertextPath; + private final Path inflatedPath; + private final Path cleartextPath; + + public NodeNames(Path ciphertextPath) { + this(ciphertextPath, null, null); + } + + private NodeNames(Path ciphertextPath, Path inflatedPath, Path cleartextPath) { + this.ciphertextPath = ciphertextPath; + this.inflatedPath = inflatedPath; + this.cleartextPath = cleartextPath; + } + + public Path getCiphertextPath() { + return ciphertextPath; + } + + public Path getInflatedPath() { + return inflatedPath; + } + + public Path getCleartextPath() { + return cleartextPath; + } + + public NodeNames withCiphertextPath(Path ciphertextPath) { + return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); + } + + public NodeNames withInflatedPath(Path inflatedPath) { + return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); + } + + public NodeNames withCleartextPath(Path cleartextPath) { + return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index 03ab9f76..ecb5cb18 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -8,10 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptofs.dir; -import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; +import com.google.common.base.Strings; import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.dir.CryptoDirectoryStream.ProcessedPaths; import org.cryptomator.cryptolib.api.Cryptor; import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeEach; @@ -19,11 +17,8 @@ import org.mockito.Mockito; import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import java.nio.file.Paths; import static org.cryptomator.cryptofs.common.Constants.SHORT_NAMES_MAX_LENGTH; import static org.hamcrest.Matchers.is; @@ -32,58 +27,47 @@ import static org.mockito.Mockito.when; public class CryptoDirectoryStreamIntegrationTest { - - private FileSystem fileSystem; - + private LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class); private CryptoDirectoryStream inTest; @BeforeEach public void setup() throws IOException { - fileSystem = Jimfs.newFileSystem(); - - Path dir = fileSystem.getPath("crapDirDoNotUse"); - Files.createDirectory(dir); - inTest = new CryptoDirectoryStream(new CiphertextDirectory("", dir), null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null); + inTest = new CryptoDirectoryStream("foo", null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null); } @Test - public void testInflateIfNeededWithShortFilename() throws IOException { + public void testInflateIfNeededWithShortFilename() { String filename = "abc"; - Path ciphertextPath = fileSystem.getPath(filename); - Files.createFile(ciphertextPath); + Path ciphertextPath = Paths.get(filename); when(longFileNameProvider.isDeflated(filename)).thenReturn(false); - ProcessedPaths paths = new ProcessedPaths(ciphertextPath); + NodeNames paths = new NodeNames(ciphertextPath); - ProcessedPaths result = inTest.inflateIfNeeded(paths); + NodeNames result = inTest.inflateIfNeeded(paths); MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); MatcherAssert.assertThat(result.getInflatedPath(), is(ciphertextPath)); MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); - MatcherAssert.assertThat(Files.exists(ciphertextPath), is(true)); } @Test public void testInflateIfNeededWithRegularLongFilename() throws IOException { String filename = "abc"; - String inflatedName = IntStream.range(0, SHORT_NAMES_MAX_LENGTH + 1).mapToObj(ignored -> "a").collect(Collectors.joining()); - Path ciphertextPath = fileSystem.getPath(filename); - Files.createFile(ciphertextPath); - Path inflatedPath = fileSystem.getPath(inflatedName); + String inflatedName = Strings.repeat("a", SHORT_NAMES_MAX_LENGTH + 1); + Path ciphertextPath = Paths.get(filename); + Path inflatedPath = Paths.get(inflatedName); when(longFileNameProvider.isDeflated(filename)).thenReturn(true); when(longFileNameProvider.inflate(ciphertextPath)).thenReturn(inflatedName); - ProcessedPaths paths = new ProcessedPaths(ciphertextPath); + NodeNames paths = new NodeNames(ciphertextPath); - ProcessedPaths result = inTest.inflateIfNeeded(paths); + NodeNames result = inTest.inflateIfNeeded(paths); MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); MatcherAssert.assertThat(result.getInflatedPath(), is(inflatedPath)); MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); - MatcherAssert.assertThat(Files.exists(ciphertextPath), is(true)); - MatcherAssert.assertThat(Files.exists(inflatedPath), is(false)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index 292e12e1..afa2e777 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -50,7 +50,6 @@ public class CryptoDirectoryStreamTest { private Cryptor cryptor; private FileNameCryptor filenameCryptor; - private Path ciphertextDirPath; private DirectoryStream dirStream; private CryptoPathMapper cryptoPathMapper; private LongFileNameProvider longFileNameProvider; @@ -58,18 +57,10 @@ public class CryptoDirectoryStreamTest { private EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern(); @BeforeEach - @SuppressWarnings("unchecked") public void setup() throws IOException { cryptor = CRYPTOR_PROVIDER.createNew(); filenameCryptor = cryptor.fileNameCryptor(); - - ciphertextDirPath = Mockito.mock(Path.class); - FileSystem fs = Mockito.mock(FileSystem.class); - Mockito.when(ciphertextDirPath.getFileSystem()).thenReturn(fs); - FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); - Mockito.when(fs.provider()).thenReturn(provider); dirStream = Mockito.mock(DirectoryStream.class); - Mockito.when(provider.newDirectoryStream(Mockito.same(ciphertextDirPath), Mockito.any())).thenReturn(dirStream); longFileNameProvider = Mockito.mock(LongFileNameProvider.class); conflictResolver = Mockito.mock(ConflictResolver.class); Mockito.when(longFileNameProvider.inflate(Mockito.any())).then(invocation -> { @@ -81,6 +72,9 @@ public void setup() throws IOException { } }); cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); + FileSystem fs = Mockito.mock(FileSystem.class); + FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); + Mockito.when(fs.provider()).thenReturn(provider); Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).then(invocation -> { Path dirFilePath = invocation.getArgument(0); if (dirFilePath.toString().contains("invalid")) { @@ -113,7 +107,7 @@ public void testDirListing() throws IOException { ciphertextFileNames.add("alsoInvalid"); Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator()); - try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, + try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertTrue(iter.hasNext()); @@ -136,7 +130,7 @@ public void testDirListingForEmptyDir() throws IOException { Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); - try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, + try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { Iterator iter = stream.iterator(); Assertions.assertFalse(iter.hasNext()); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index dfba393c..565f4947 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -38,7 +38,7 @@ public class DirectoryStreamFactoryTest { @BeforeEach public void setup() throws IOException { when(directoryStreamBuilder.cleartextPath(Mockito.any())).thenReturn(directoryStreamBuilder); - when(directoryStreamBuilder.ciphertextDirectory(Mockito.any())).thenReturn(directoryStreamBuilder); + when(directoryStreamBuilder.dirId(Mockito.any())).thenReturn(directoryStreamBuilder); when(directoryStreamBuilder.ciphertextDirectoryStream(Mockito.any())).thenReturn(directoryStreamBuilder); when(directoryStreamBuilder.filter(Mockito.any())).thenReturn(directoryStreamBuilder); when(directoryStreamBuilder.onClose(Mockito.any())).thenReturn(directoryStreamBuilder); From 172ecad4da6bdc7666f18158a53116cbb37cc743 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Oct 2019 14:31:20 +0200 Subject: [PATCH 47/62] Renamed constant --- .../java/org/cryptomator/cryptofs/CryptoPathMapper.java | 2 +- .../java/org/cryptomator/cryptofs/common/Constants.java | 2 +- .../DeleteNonEmptyCiphertextDirectoryIntegrationTest.java | 7 +++---- .../org/cryptomator/cryptofs/dir/ConflictResolverTest.java | 2 +- .../cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java | 4 ++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index 707bc796..df2a81cb 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -121,7 +121,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) { String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parentDirId, cleartextName)); Path c9rPath = parentCiphertextDir.resolve(ciphertextName); - if (ciphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH) { + if (ciphertextName.length() > Constants.MAX_CIPHERTEXT_NAME_LENGTH) { LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath); return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName)); } else { diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java index 33b53f03..27ca3651 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -14,7 +14,7 @@ public final class Constants { public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; public static final String DATA_DIR_NAME = "d"; - public static final int SHORT_NAMES_MAX_LENGTH = 222; // calculations done in https://github.com/cryptomator/cryptofs/issues/60 + public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 public static final String ROOT_DIR_ID = ""; public static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; public static final String DEFLATED_FILE_SUFFIX = ".c9s"; diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index f2d5d464..7ef1cd47 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +import com.google.common.base.Strings; import org.cryptomator.cryptofs.common.Constants; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -26,7 +27,7 @@ import java.util.stream.Stream; import static java.nio.file.StandardOpenOption.CREATE_NEW; -import static org.cryptomator.cryptofs.common.Constants.SHORT_NAMES_MAX_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; @@ -120,9 +121,7 @@ public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException // a // .. LongNameaaa... - String name = "LongName" + IntStream.range(0, SHORT_NAMES_MAX_LENGTH) // - .mapToObj(ignored -> "a") // - .collect(Collectors.joining()); + String name = "LongName" + Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH); createFolder(cleartextDirectory, name); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { diff --git a/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java index b8200092..9b167fb2 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java @@ -145,7 +145,7 @@ public void testResolveByRenamingRegularFile() throws IOException { public void testResolveByRenamingShortenedFile() throws IOException { String conflictingName = "FooBar== (2).c9s"; String canonicalName = "FooBar==.c9s"; - String inflatedName = Strings.repeat("a", Constants.SHORT_NAMES_MAX_LENGTH + 1); + String inflatedName = Strings.repeat("a", Constants.MAX_CIPHERTEXT_NAME_LENGTH + 1); Path conflictingPath = tmpDir.resolve(conflictingName); Path canonicalPath = tmpDir.resolve(canonicalName); Files.write(conflictingPath, new byte[3]); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index ecb5cb18..d755f0f5 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -20,7 +20,7 @@ import java.nio.file.Path; import java.nio.file.Paths; -import static org.cryptomator.cryptofs.common.Constants.SHORT_NAMES_MAX_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; @@ -55,7 +55,7 @@ public void testInflateIfNeededWithShortFilename() { @Test public void testInflateIfNeededWithRegularLongFilename() throws IOException { String filename = "abc"; - String inflatedName = Strings.repeat("a", SHORT_NAMES_MAX_LENGTH + 1); + String inflatedName = Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH + 1); Path ciphertextPath = Paths.get(filename); Path inflatedPath = Paths.get(inflatedName); when(longFileNameProvider.isDeflated(filename)).thenReturn(true); From 90cb839053717cace2cb14b06085feda8656a879 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Oct 2019 19:30:43 +0200 Subject: [PATCH 48/62] Refactored directory listing for issue #65 We can now find valid ciphertext in filenames even in cases such as "validCiph3rtext_conflict" where _conflict is part of the BASE64 alphabet. Also, directory listing will get several specialized classes for edge cases such as .c9s files, conflict handling for symlinks and directories, etc. --- .../cryptofs/common/Constants.java | 2 + .../cryptofs/dir/C9rConflictResolver.java | 163 ++++++++++++++++++ .../cryptofs/dir/C9rDecryptor.java | 87 ++++++++++ .../cryptofs/dir/C9rProcessor.java | 22 +++ .../cryptofs/dir/ConflictResolver.java | 1 + .../cryptofs/dir/CryptoDirectoryStream.java | 138 ++++----------- .../cryptofs/dir/EncryptedNamePattern.java | 29 ---- .../cryptomator/cryptofs/dir/NodeNames.java | 41 +---- .../cryptofs/dir/NodeProcessor.java | 28 +++ ...ptyCiphertextDirectoryIntegrationTest.java | 4 + .../cryptofs/dir/C9rDecryptorTest.java | 116 +++++++++++++ .../CryptoDirectoryStreamIntegrationTest.java | 77 ++++----- .../dir/CryptoDirectoryStreamTest.java | 114 +++++------- .../dir/EncryptedNamePatternTest.java | 39 ----- 14 files changed, 539 insertions(+), 322 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java delete mode 100644 src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java create mode 100644 src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java delete mode 100644 src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java index 27ca3651..69dc3eb7 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -15,6 +15,7 @@ public final class Constants { public static final String DATA_DIR_NAME = "d"; public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 + public static final int MAX_CLEARTEXT_NAME_LENGTH = 146; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303 public static final String ROOT_DIR_ID = ""; public static final String CRYPTOMATOR_FILE_SUFFIX = ".c9r"; public static final String DEFLATED_FILE_SUFFIX = ".c9s"; @@ -24,6 +25,7 @@ public final class Constants { public static final String INFLATED_FILE_NAME = "name.c9s"; public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1 + public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars public static final String SEPARATOR = "/"; diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java new file mode 100644 index 00000000..9bdf210a --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -0,0 +1,163 @@ +package org.cryptomator.cryptofs.dir; + +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.stream.Stream; + +import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME; +import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.MAX_CLEARTEXT_NAME_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.MAX_DIR_FILE_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH; +import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME; + +@DirectoryStreamScoped +class C9rConflictResolver { + + private static final Logger LOG = LoggerFactory.getLogger(C9rConflictResolver.class); + + private final Cryptor cryptor; + private final byte[] dirId; + + @Inject + public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId) { + this.cryptor = cryptor; + this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); + } + + public Stream process(NodeNames nodeNames) { + Preconditions.checkArgument(nodeNames.ciphertextName != null, "Can only resolve conflicts if ciphertextName is set"); + Preconditions.checkArgument(nodeNames.cleartextName != null, "Can only resolve conflicts if cleartextName is set"); + + String canonicalName = nodeNames.ciphertextName + Constants.CRYPTOMATOR_FILE_SUFFIX; + if (nodeNames.ciphertextFileName.equals(canonicalName)) { + return Stream.of(nodeNames); + } else { + try { + Path canonicalPath = nodeNames.ciphertextPath.resolveSibling(canonicalName); + return resolveConflict(nodeNames, canonicalPath); + } catch (IOException e) { + LOG.error("Failed to resolve conflict for " + nodeNames.ciphertextPath, e); + return Stream.empty(); + } + } + } + + private Stream resolveConflict(NodeNames nodeNames, Path canonicalPath) throws IOException { + Path conflictingPath = nodeNames.ciphertextPath; + if (resolveConflictTrivially(canonicalPath, conflictingPath)) { + NodeNames resolvedNames = new NodeNames(canonicalPath); + resolvedNames.cleartextName = nodeNames.cleartextName; + resolvedNames.ciphertextName = nodeNames.ciphertextName; + return Stream.of(resolvedNames); + } else { + return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, nodeNames.cleartextName)); + } + } + + /** + * Resolves a conflict by renaming the conflicting file. + * + * @param canonicalPath The path to the original (conflict-free) file. + * @param conflictingPath The path to the potentially conflicting file. + * @param cleartext The cleartext name of the conflicting file. + * @return The NodeNames for the newly created node after renaming the conflicting file. + * @throws IOException + */ + private NodeNames renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException { + assert Files.exists(canonicalPath); + final int beginOfFileExtension = cleartext.lastIndexOf('.'); + final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : ""; + final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext; + final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), MAX_CLEARTEXT_NAME_LENGTH - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)" + String alternativeCleartext; + String alternativeCiphertext; + String alternativeCiphertextName; + Path alternativePath; + int i = 1; + do { + alternativeCleartext = lengthRestrictedBasename + " (" + i++ + ")" + fileExtension; + alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId); + alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX; + alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName); + } while (Files.exists(alternativePath)); + assert alternativeCiphertextName.length() <= MAX_CIPHERTEXT_NAME_LENGTH; + LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); + Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); + NodeNames nodeNames = new NodeNames(alternativePath); + nodeNames.cleartextName = alternativeCleartext; + nodeNames.ciphertextName = alternativeCiphertext; + return nodeNames; + } + + + /** + * Tries to resolve a conflicting file without renaming the file. If successful, only the file with the canonical path will exist afterwards. + * + * @param canonicalPath The path to the original (conflict-free) resource (must not exist). + * @param conflictingPath The path to the potentially conflicting file (known to exist). + * @return true if the conflict has been resolved. + * @throws IOException + */ + private boolean resolveConflictTrivially(Path canonicalPath, Path conflictingPath) throws IOException { + if (!Files.exists(canonicalPath)) { + Files.move(conflictingPath, canonicalPath); // boom. conflict solved. + return true; + } else if (hasSameFileContent(conflictingPath.resolve(DIR_FILE_NAME), canonicalPath.resolve(DIR_FILE_NAME), MAX_DIR_FILE_LENGTH)) { + LOG.info("Removing conflicting directory {} (identical to {})", conflictingPath, canonicalPath); + MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); + return true; + } else if (hasSameFileContent(conflictingPath.resolve(SYMLINK_FILE_NAME), canonicalPath.resolve(SYMLINK_FILE_NAME), MAX_SYMLINK_LENGTH)) { + LOG.info("Removing conflicting symlink {} (identical to {})", conflictingPath, canonicalPath); + MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); + return true; + } else { + return false; + } + } + + /** + * @param conflictingPath Path to a potentially conflicting file supposedly containing a directory id + * @param canonicalPath Path to the canonical file containing a directory id + * @param numBytesToCompare Number of bytes to read from each file and compare to each other. + * @return true if the first numBytesToCompare bytes are equal in both files. + * @throws IOException If an I/O exception occurs while reading either file. + */ + private boolean hasSameFileContent(Path conflictingPath, Path canonicalPath, int numBytesToCompare) throws IOException { + if (!Files.isDirectory(conflictingPath.getParent()) || !Files.isDirectory(canonicalPath.getParent())) { + return false; + } + try (ReadableByteChannel in1 = Files.newByteChannel(conflictingPath, StandardOpenOption.READ); // + ReadableByteChannel in2 = Files.newByteChannel(canonicalPath, StandardOpenOption.READ)) { + ByteBuffer buf1 = ByteBuffer.allocate(numBytesToCompare); + ByteBuffer buf2 = ByteBuffer.allocate(numBytesToCompare); + int read1 = in1.read(buf1); + int read2 = in2.read(buf2); + buf1.flip(); + buf2.flip(); + return read1 == read2 && buf1.compareTo(buf2) == 0; + } catch (NoSuchFileException e) { + return false; + } + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java new file mode 100644 index 00000000..90c65b12 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java @@ -0,0 +1,87 @@ +package org.cryptomator.cryptofs.dir; + +import com.google.common.base.CharMatcher; +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.StringUtils; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; + +import javax.inject.Inject; +import javax.inject.Named; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class C9rDecryptor { + + // visible for testing: + static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}"); + private static final CharMatcher DELIM_MATCHER = CharMatcher.anyOf("_-"); + + private final Cryptor cryptor; + private final byte[] dirId; + + @Inject + public C9rDecryptor(Cryptor cryptor, @Named("dirId") String dirId) { + this.cryptor = cryptor; + this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); + } + + public Stream process(NodeNames nodeNames) { + String basename = StringUtils.removeEnd(nodeNames.ciphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX); + Matcher matcher = BASE64_PATTERN.matcher(basename); + Optional match = extractCiphertext(nodeNames, matcher, 0, basename.length()); + if (match.isPresent()) { + return Stream.of(match.get()); + } else { + return Stream.empty(); + } + } + + private Optional extractCiphertext(NodeNames nodeNames, Matcher matcher, int start, int end) { + matcher.region(start, end); + if (matcher.find()) { + final MatchResult matchResult = matcher.toMatchResult(); + final String validBase64 = matchResult.group(); + assert validBase64.length() >= 24; + assert matchResult.end() - matchResult.start() >= 24; + try { + nodeNames.cleartextName = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), validBase64, dirId); + nodeNames.ciphertextName = validBase64; + return Optional.of(nodeNames); + } catch (AuthenticationFailedException e) { + // narrow down to sub-base64-sequences: + int firstDelimIdx = DELIM_MATCHER.indexIn(validBase64); + int lastDelimIdx = DELIM_MATCHER.lastIndexIn(validBase64); + if (firstDelimIdx == -1) { + assert lastDelimIdx == -1; + return Optional.empty(); + } + assert firstDelimIdx != -1; + assert lastDelimIdx != -1; + Optional subsequenceMatch = Optional.empty(); + if (!subsequenceMatch.isPresent() && firstDelimIdx == 0) { + subsequenceMatch = extractCiphertext(nodeNames, matcher, matchResult.start() + 1, end); + } + if (!subsequenceMatch.isPresent() && lastDelimIdx == validBase64.length() - 1) { + subsequenceMatch = extractCiphertext(nodeNames, matcher, start, matchResult.end() - 1); + } + if (!subsequenceMatch.isPresent() && firstDelimIdx > 0) { + subsequenceMatch = extractCiphertext(nodeNames, matcher, matchResult.start() + firstDelimIdx, end); + } + if (!subsequenceMatch.isPresent() && lastDelimIdx < validBase64.length() - 1) { + subsequenceMatch = extractCiphertext(nodeNames, matcher, start, matchResult.start() + lastDelimIdx); + } + return subsequenceMatch; + } + } else { + return Optional.empty(); + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java new file mode 100644 index 00000000..7e613fa8 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java @@ -0,0 +1,22 @@ +package org.cryptomator.cryptofs.dir; + +import javax.inject.Inject; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class C9rProcessor { + + private final C9rDecryptor decryptor; + private final C9rConflictResolver conflictResolver; + + @Inject + public C9rProcessor(C9rDecryptor decryptor, C9rConflictResolver conflictResolver){ + this.decryptor = decryptor; + this.conflictResolver = conflictResolver; + } + + public Stream process(NodeNames nodeNames) { + return decryptor.process(nodeNames).flatMap(conflictResolver::process); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java index a0c582d3..58eff2ac 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java @@ -32,6 +32,7 @@ import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH; import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME; +@Deprecated @CryptoFileSystemScoped class ConflictResolver { diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index c9837f95..13939ac9 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -8,28 +8,16 @@ *******************************************************************************/ package org.cryptomator.cryptofs.dir; -import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryIteratorException; import java.nio.file.DirectoryStream; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Iterator; -import java.util.Objects; -import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -42,28 +30,19 @@ public class CryptoDirectoryStream implements DirectoryStream { private final String directoryId; private final DirectoryStream ciphertextDirStream; private final Path cleartextDir; - private final FileNameCryptor filenameCryptor; - private final CryptoPathMapper cryptoPathMapper; - private final LongFileNameProvider longFileNameProvider; - private final ConflictResolver conflictResolver; private final DirectoryStream.Filter filter; private final Consumer onClose; - private final EncryptedNamePattern encryptedNamePattern; + private final NodeProcessor nodeProcessor; @Inject - public CryptoDirectoryStream(@Named("dirId") String dirId, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, Cryptor cryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, - ConflictResolver conflictResolver, DirectoryStream.Filter filter, Consumer onClose, EncryptedNamePattern encryptedNamePattern) { + public CryptoDirectoryStream(@Named("dirId") String dirId, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, DirectoryStream.Filter filter, Consumer onClose, NodeProcessor nodeProcessor) { LOG.trace("OPEN {}", dirId); - this.onClose = onClose; - this.encryptedNamePattern = encryptedNamePattern; this.directoryId = dirId; this.ciphertextDirStream = ciphertextDirStream; this.cleartextDir = cleartextDir; - this.filenameCryptor = cryptor.fileNameCryptor(); - this.cryptoPathMapper = cryptoPathMapper; - this.longFileNameProvider = longFileNameProvider; - this.conflictResolver = conflictResolver; this.filter = filter; + this.onClose = onClose; + this.nodeProcessor = nodeProcessor; } @Override @@ -72,91 +51,48 @@ public Iterator iterator() { } private Stream cleartextDirectoryListing() { - return directoryListing() // - .map(NodeNames::getCleartextPath) // + return directoryListing() + .map(node -> cleartextDir.resolve(node.cleartextName)) .filter(this::isAcceptableByFilter); } public Stream ciphertextDirectoryListing() { - return directoryListing().map(NodeNames::getCiphertextPath); + return directoryListing().map(node -> node.ciphertextPath); } private Stream directoryListing() { - Stream pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(NodeNames::new); - Stream resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull); - Stream inflated = resolved.map(this::inflateIfNeeded).filter(Objects::nonNull); - Stream decrypted = inflated.map(this::decrypt).filter(Objects::nonNull); - Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); - return sanitized; + return StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(NodeNames::new).flatMap(nodeProcessor::process); +// Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); +// return sanitized; } - private NodeNames resolveConflictingFileIfNeeded(NodeNames paths) { - try { - return paths.withCiphertextPath(conflictResolver.resolveConflictsIfNecessary(paths.getCiphertextPath(), directoryId)); - } catch (IOException e) { - LOG.warn("I/O exception while finding potentially conflicting file versions for {}.", paths.getCiphertextPath()); - return null; - } - } - - NodeNames inflateIfNeeded(NodeNames paths) { - String fileName = paths.getCiphertextPath().getFileName().toString(); - if (longFileNameProvider.isDeflated(fileName)) { - try { - String longFileName = longFileNameProvider.inflate(paths.getCiphertextPath()); - return paths.withInflatedPath(paths.getCiphertextPath().resolveSibling(longFileName)); - } catch (IOException e) { - LOG.warn(paths.getCiphertextPath() + " could not be inflated."); - return null; - } - } else { - return paths.withInflatedPath(paths.getCiphertextPath()); - } - } - - private NodeNames decrypt(NodeNames paths) { - Optional ciphertextName = encryptedNamePattern.extractEncryptedName(paths.getInflatedPath()); - if (ciphertextName.isPresent()) { - String ciphertext = ciphertextName.get(); - try { - String cleartext = filenameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, directoryId.getBytes(StandardCharsets.UTF_8)); - return paths.withCleartextPath(cleartextDir.resolve(cleartext)); - } catch (AuthenticationFailedException e) { - LOG.warn(paths.getInflatedPath() + " not decryptable due to an unauthentic ciphertext."); - return null; - } - } else { - return null; - } - } - - /** - * Checks if a given file belongs into this ciphertext dir. - * - * @param paths The path to check. - * @return true if the file is an existing ciphertext or directory file. - */ - private boolean passesPlausibilityChecks(NodeNames paths) { - return !isBrokenDirectoryFile(paths); - } - - private boolean isBrokenDirectoryFile(NodeNames paths) { - Path potentialDirectoryFile = paths.getCiphertextPath().resolve(Constants.DIR_FILE_NAME); - if (Files.isRegularFile(potentialDirectoryFile)) { - final Path dirPath; - try { - dirPath = cryptoPathMapper.resolveDirectory(potentialDirectoryFile).path; - } catch (IOException e) { - LOG.warn("Broken directory file {}. Exception: {}", potentialDirectoryFile, e.getMessage()); - return true; - } - if (!Files.isDirectory(dirPath)) { - LOG.warn("Broken directory file {}. Directory {} does not exist.", potentialDirectoryFile, dirPath); - return true; - } - } - return false; - } +// /** +// * Checks if a given file belongs into this ciphertext dir. +// * +// * @param paths The path to check. +// * @return true if the file is an existing ciphertext or directory file. +// */ +// private boolean passesPlausibilityChecks(NodeNames paths) { +// return !isBrokenDirectoryFile(paths); +// } +// +// private boolean isBrokenDirectoryFile(NodeNames paths) { +// Path potentialDirectoryFile = paths.getCiphertextPath().resolve(Constants.DIR_FILE_NAME); +// if (Files.isRegularFile(potentialDirectoryFile)) { +// final Path dirPath; +// try { +// dirPath = cryptoPathMapper.resolveDirectory(potentialDirectoryFile).path; +// } catch (IOException e) { +// LOG.warn("Broken directory file {}. Exception: {}", potentialDirectoryFile, e.getMessage()); +// return true; +// } +// if (!Files.isDirectory(dirPath)) { +// LOG.warn("Broken directory file {}. Directory {} does not exist.", potentialDirectoryFile, dirPath); +// return true; +// } +// } +// return false; +// } private boolean isAcceptableByFilter(Path path) { try { diff --git a/src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java deleted file mode 100644 index 29acd1fb..00000000 --- a/src/main/java/org/cryptomator/cryptofs/dir/EncryptedNamePattern.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.cryptomator.cryptofs.dir; - -import javax.inject.Inject; -import javax.inject.Singleton; -import java.nio.file.Path; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Singleton -class EncryptedNamePattern { - - private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}"); - - @Inject - public EncryptedNamePattern() { - } - - public Optional extractEncryptedName(Path ciphertextFile) { - String name = ciphertextFile.getFileName().toString(); - Matcher matcher = BASE64_PATTERN.matcher(name); - if (matcher.find(0)) { - return Optional.of(matcher.group()); - } else { - return Optional.empty(); - } - } - -} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java index ae5e0bfe..2fc15021 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java @@ -1,45 +1,18 @@ package org.cryptomator.cryptofs.dir; import java.nio.file.Path; +import java.util.Objects; class NodeNames { - private final Path ciphertextPath; - private final Path inflatedPath; - private final Path cleartextPath; + public final Path ciphertextPath; + public final String ciphertextFileName; + public String ciphertextName; + public String cleartextName; public NodeNames(Path ciphertextPath) { - this(ciphertextPath, null, null); - } - - private NodeNames(Path ciphertextPath, Path inflatedPath, Path cleartextPath) { - this.ciphertextPath = ciphertextPath; - this.inflatedPath = inflatedPath; - this.cleartextPath = cleartextPath; - } - - public Path getCiphertextPath() { - return ciphertextPath; - } - - public Path getInflatedPath() { - return inflatedPath; - } - - public Path getCleartextPath() { - return cleartextPath; - } - - public NodeNames withCiphertextPath(Path ciphertextPath) { - return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); - } - - public NodeNames withInflatedPath(Path inflatedPath) { - return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); - } - - public NodeNames withCleartextPath(Path cleartextPath) { - return new NodeNames(ciphertextPath, inflatedPath, cleartextPath); + this.ciphertextPath = Objects.requireNonNull(ciphertextPath); + this.ciphertextFileName = ciphertextPath.getFileName().toString(); } } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java new file mode 100644 index 00000000..a67af665 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java @@ -0,0 +1,28 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.common.Constants; + +import javax.inject.Inject; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class NodeProcessor { + + private final C9rProcessor c9rProcessor; + + @Inject + public NodeProcessor(C9rProcessor c9rProcessor){ + this.c9rProcessor = c9rProcessor; + } + + public Stream process(NodeNames nodeNames) { + if (nodeNames.ciphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX)) { + return c9rProcessor.process(nodeNames); + } else if (nodeNames.ciphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX)) { + return Stream.empty(); + } else { + return Stream.empty(); + } + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index 7ef1cd47..d8d47c54 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -12,6 +12,7 @@ import org.cryptomator.cryptofs.common.Constants; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -79,6 +80,7 @@ public void testDeleteCiphertextDirectoryContainingDirectories() throws IOExcept } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IOException { Path cleartextDirectory = fileSystem.getPath("/b"); Files.createDirectory(cleartextDirectory); @@ -91,6 +93,7 @@ public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IO } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() throws IOException { Path cleartextDirectory = fileSystem.getPath("/c"); Files.createDirectory(cleartextDirectory); @@ -115,6 +118,7 @@ public void testDeleteNonEmptyDir() throws IOException { } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException { Path cleartextDirectory = fileSystem.getPath("/e"); Files.createDirectory(cleartextDirectory); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java new file mode 100644 index 00000000..6ddfd320 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java @@ -0,0 +1,116 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Stream; + +class C9rDecryptorTest { + + private Cryptor cryptor; + private FileNameCryptor fileNameCryptor; + private C9rDecryptor decryptor; + + @BeforeEach + public void setup() { + cryptor = Mockito.mock(Cryptor.class); + fileNameCryptor = Mockito.mock(FileNameCryptor.class); + Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).then(invocation -> { + String ciphertext = invocation.getArgument(1); + if (ciphertext.equals("aaaaBBBBccccDDDDeeeeFFFF")) { + return "FFFFeeeeDDDDccccBBBBaaaa"; + } else { + throw new AuthenticationFailedException("Invalid ciphertext " + ciphertext); + } + }); + decryptor = new C9rDecryptor(cryptor, "foo"); + } + + @ParameterizedTest + @ValueSource(strings = { + "aaaaBBBBccccDDDDeeeeFFFF", + "aaaaBBBBccccDDDDeeeeFFF=", + "aaaaBBBBccccDDDDeeeeFF==", + "aaaaBBBBccccDDDDeeeeF===", + "aaaaBBBBccccDDDDeeee====", + "aaaaBBBB0123456789-_====", + "aaaaBBBBccccDDDDeeeeFFFFggggHH==", + }) + public void testValidBase64Pattern(String input) { + Assertions.assertTrue(C9rDecryptor.BASE64_PATTERN.matcher(input).matches()); + } + + @ParameterizedTest + @ValueSource(strings = { + "aaaaBBBBccccDDDDeeee", // too short + "aaaaBBBBccccDDDDeeeeFFF", // unpadded + "====BBBBccccDDDDeeeeFFFF", // padding not at end + "????BBBBccccDDDDeeeeFFFF", // invalid chars + "conflict aaaaBBBBccccDDDDeeeeFFFF", // only a partial match + "aaaaBBBBccccDDDDeeeeFFFF conflict", // only a partial match + }) + public void testInvalidBase64Pattern(String input) { + Assertions.assertFalse(C9rDecryptor.BASE64_PATTERN.matcher(input).matches()); + } + + @Test + @DisplayName("process canonical filename") + public void testProcessFullMatch() { + NodeNames input = new NodeNames(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); + + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); + + Assertions.assertTrue(optionalResult.isPresent()); + Assertions.assertEquals("FFFFeeeeDDDDccccBBBBaaaa", optionalResult.get().cleartextName); + } + + @DisplayName("process non-canonical filename") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "aaaaBBBBccccDDDDeeeeFFFF (conflict3000).c9r", + "(conflict3000) aaaaBBBBccccDDDDeeeeFFFF.c9r", + "conflict_aaaaBBBBccccDDDDeeeeFFFF.c9r", + "aaaaBBBBccccDDDDeeeeFFFF_conflict.c9r", + "_aaaaBBBBccccDDDDeeeeFFFF.c9r", + "aaaaBBBBccccDDDDeeeeFFFF_.c9r", + }) + public void testProcessPartialMatch(String filename) { + NodeNames input = new NodeNames(Paths.get(filename)); + + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); + + Assertions.assertTrue(optionalResult.isPresent()); + Assertions.assertEquals("FFFFeeeeDDDDccccBBBBaaaa", optionalResult.get().cleartextName); + } + + @DisplayName("process filename without ciphertext") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "foo.bar", + "foo.c9r", + "aaaaBBBB????DDDDeeeeFFFF.c9r", + "aaaaBBBBxxxxDDDDeeeeFFFF.c9r", + }) + public void testProcessNoMatch(String filename) { + NodeNames input = new NodeNames(Paths.get(filename)); + + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); + + Assertions.assertFalse(optionalResult.isPresent()); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java index d755f0f5..cc829535 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java @@ -8,23 +8,12 @@ *******************************************************************************/ package org.cryptomator.cryptofs.dir; -import com.google.common.base.Strings; import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptolib.api.Cryptor; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; -import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class CryptoDirectoryStreamIntegrationTest { @@ -34,40 +23,40 @@ public class CryptoDirectoryStreamIntegrationTest { @BeforeEach public void setup() throws IOException { - inTest = new CryptoDirectoryStream("foo", null,null, Mockito.mock(Cryptor.class), null, longFileNameProvider, null, null, null, null); + inTest = new CryptoDirectoryStream("foo", null,null, null, null, null); } - @Test - public void testInflateIfNeededWithShortFilename() { - String filename = "abc"; - Path ciphertextPath = Paths.get(filename); - when(longFileNameProvider.isDeflated(filename)).thenReturn(false); - - NodeNames paths = new NodeNames(ciphertextPath); - - NodeNames result = inTest.inflateIfNeeded(paths); - - MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); - MatcherAssert.assertThat(result.getInflatedPath(), is(ciphertextPath)); - MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); - } - - @Test - public void testInflateIfNeededWithRegularLongFilename() throws IOException { - String filename = "abc"; - String inflatedName = Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH + 1); - Path ciphertextPath = Paths.get(filename); - Path inflatedPath = Paths.get(inflatedName); - when(longFileNameProvider.isDeflated(filename)).thenReturn(true); - when(longFileNameProvider.inflate(ciphertextPath)).thenReturn(inflatedName); - - NodeNames paths = new NodeNames(ciphertextPath); - - NodeNames result = inTest.inflateIfNeeded(paths); - - MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); - MatcherAssert.assertThat(result.getInflatedPath(), is(inflatedPath)); - MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); - } +// @Test +// public void testInflateIfNeededWithShortFilename() { +// String filename = "abc"; +// Path ciphertextPath = Paths.get(filename); +// when(longFileNameProvider.isDeflated(filename)).thenReturn(false); +// +// NodeNames paths = new NodeNames(ciphertextPath); +// +// NodeNames result = inTest.inflateIfNeeded(paths); +// +// MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); +// MatcherAssert.assertThat(result.getInflatedPath(), is(ciphertextPath)); +// MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); +// } +// +// @Test +// public void testInflateIfNeededWithRegularLongFilename() throws IOException { +// String filename = "abc"; +// String inflatedName = Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH + 1); +// Path ciphertextPath = Paths.get(filename); +// Path inflatedPath = Paths.get(inflatedName); +// when(longFileNameProvider.isDeflated(filename)).thenReturn(true); +// when(longFileNameProvider.inflate(ciphertextPath)).thenReturn(inflatedName); +// +// NodeNames paths = new NodeNames(ciphertextPath); +// +// NodeNames result = inTest.inflateIfNeeded(paths); +// +// MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); +// MatcherAssert.assertThat(result.getInflatedPath(), is(inflatedPath)); +// MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); +// } } diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index afa2e777..de2adf7c 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -8,116 +8,81 @@ *******************************************************************************/ package org.cryptomator.cryptofs.dir; -import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptofs.common.StringUtils; -import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.Cryptors; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.CryptorProvider; -import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; -import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.spi.FileSystemProvider; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Spliterators; import java.util.function.Consumer; - -import static org.mockito.AdditionalAnswers.returnsFirstArg; +import java.util.stream.Stream; public class CryptoDirectoryStreamTest { private static final Consumer DO_NOTHING_ON_CLOSE = ignored -> { }; private static final Filter ACCEPT_ALL = ignored -> true; - private static CryptorProvider CRYPTOR_PROVIDER = Cryptors.version1(NullSecureRandom.INSTANCE); - - private Cryptor cryptor; - private FileNameCryptor filenameCryptor; + + private NodeProcessor nodeProcessor; private DirectoryStream dirStream; - private CryptoPathMapper cryptoPathMapper; - private LongFileNameProvider longFileNameProvider; - private ConflictResolver conflictResolver; - private EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern(); @BeforeEach public void setup() throws IOException { - cryptor = CRYPTOR_PROVIDER.createNew(); - filenameCryptor = cryptor.fileNameCryptor(); + nodeProcessor = Mockito.mock(NodeProcessor.class); dirStream = Mockito.mock(DirectoryStream.class); - longFileNameProvider = Mockito.mock(LongFileNameProvider.class); - conflictResolver = Mockito.mock(ConflictResolver.class); - Mockito.when(longFileNameProvider.inflate(Mockito.any())).then(invocation -> { - String shortName = invocation.getArgument(0); - if (shortName.contains("invalid")) { - throw new IOException("invalid shortened name"); - } else { - return StringUtils.removeEnd(shortName, ".lng"); - } - }); - cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); - FileSystem fs = Mockito.mock(FileSystem.class); - FileSystemProvider provider = Mockito.mock(FileSystemProvider.class); - Mockito.when(fs.provider()).thenReturn(provider); - Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).then(invocation -> { - Path dirFilePath = invocation.getArgument(0); - if (dirFilePath.toString().contains("invalid")) { - throw new IOException("Invalid directory."); - } - Path dirPath = Mockito.mock(Path.class); - BasicFileAttributes attrs = Mockito.mock(BasicFileAttributes.class); - Mockito.when(dirPath.getFileSystem()).thenReturn(fs); - Mockito.when(provider.readAttributes(dirPath, BasicFileAttributes.class)).thenReturn(attrs); - Mockito.when(attrs.isDirectory()).thenReturn(!dirFilePath.toString().contains("noDirectory")); - return new CiphertextDirectory("asdf", dirPath); - }); - - Mockito.when(conflictResolver.resolveConflictsIfNecessary(Mockito.any(), Mockito.any())).then(returnsFirstArg()); + } + + private ArgumentMatcher nodeNamed(String name) { + return node -> node.ciphertextFileName.equals(name); } @Test public void testDirListing() throws IOException { + Path ciphertextPath = Paths.get("/f00/b4r"); Path cleartextPath = Paths.get("/foo/bar"); - List ciphertextFileNames = new ArrayList<>(); - ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(), "one", "foo".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"two", "foo".getBytes()) + " (conflict)" + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add("?" + filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"three", "foo".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add("0invalidDirectory" + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add("0noDirectory" + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add("invalidLongName.lng" + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"four", "foo".getBytes()) + ".lng" + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add(filenameCryptor.encryptFilename(BaseEncoding.base64Url(),"invalid", "bar".getBytes()) + Constants.CRYPTOMATOR_FILE_SUFFIX); - ciphertextFileNames.add("alsoInvalid"); - Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator()); - - try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, - DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { + ciphertextFileNames.add("ciphertextFile1"); + ciphertextFileNames.add("ciphertextFile2"); + ciphertextFileNames.add("ciphertextDirectory1"); + ciphertextFileNames.add("invalidCiphertext"); + Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(ciphertextPath::resolve).spliterator()); + Mockito.doAnswer(invocation -> { + NodeNames nodeNames = invocation.getArgument(0); + nodeNames.cleartextName = "cleartextFile1"; + return Stream.of(nodeNames); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextFile1"))); + Mockito.doAnswer(invocation -> { + NodeNames nodeNames = invocation.getArgument(0); + nodeNames.cleartextName = "cleartextFile2"; + return Stream.of(nodeNames); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextFile2"))); + Mockito.doAnswer(invocation -> { + NodeNames nodeNames = invocation.getArgument(0); + nodeNames.cleartextName = "cleartextDirectory1"; + return Stream.of(nodeNames); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextDirectory1"))); + Mockito.doAnswer(invocation -> { + return Stream.empty(); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("invalidCiphertext"))); + + try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, nodeProcessor)) { Iterator iter = stream.iterator(); Assertions.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("one"), iter.next()); - Assertions.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("two"), iter.next()); + Assertions.assertEquals(cleartextPath.resolve("cleartextFile1"), iter.next()); Assertions.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("three"), iter.next()); + Assertions.assertEquals(cleartextPath.resolve("cleartextFile2"), iter.next()); Assertions.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("four"), iter.next()); + Assertions.assertEquals(cleartextPath.resolve("cleartextDirectory1"), iter.next()); Assertions.assertFalse(iter.hasNext()); Mockito.verify(dirStream, Mockito.never()).close(); } @@ -130,8 +95,7 @@ public void testDirListingForEmptyDir() throws IOException { Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); - try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, cryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, - DO_NOTHING_ON_CLOSE, encryptedNamePattern)) { + try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, nodeProcessor)) { Iterator iter = stream.iterator(); Assertions.assertFalse(iter.hasNext()); Assertions.assertThrows(NoSuchElementException.class, () -> { diff --git a/src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java deleted file mode 100644 index 109d1f0f..00000000 --- a/src/test/java/org/cryptomator/cryptofs/dir/EncryptedNamePatternTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cryptomator.cryptofs.dir; - -import org.cryptomator.cryptofs.dir.EncryptedNamePattern; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.nio.file.Paths; -import java.util.Optional; - -public class EncryptedNamePatternTest { - - private EncryptedNamePattern inTest = new EncryptedNamePattern(); - - @ParameterizedTest - @ValueSource(strings = { - "aaaaBBBBcccc0000----__==", - "?aaaaBBBBcccc0000----__==", - "aaaaBBBBcccc0000----__== (conflict)", - "?aaaaBBBBcccc0000----__== (conflict)", - }) - public void testValidCiphertextNames(String name) { - Optional result = inTest.extractEncryptedName(Paths.get(name)); - - Assertions.assertTrue(result.isPresent()); - } - - @ParameterizedTest - @ValueSource(strings = { - "tooShort", - "aaaaBBBB====0000----__==", - }) - public void testInvalidCiphertextNames(String name) { - Optional result = inTest.extractEncryptedName(Paths.get(name)); - - Assertions.assertFalse(result.isPresent()); - } - -} From dcc58cd221978ca3e3a48480e010814969e54dec Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Oct 2019 20:01:27 +0200 Subject: [PATCH 49/62] updated dependencies (because owasp dependency check failed...) --- pom.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 1e79a5e5..fa39428a 100644 --- a/pom.xml +++ b/pom.xml @@ -20,8 +20,8 @@ 1.7.28 5.5.2 - 3.0.0 - 2.1 + 3.1.0 + 2.2 UTF-8 @@ -129,7 +129,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.8.1 8 true @@ -145,7 +145,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.1 + 2.22.2 @@ -158,7 +158,7 @@ org.owasp dependency-check-maven - 4.0.2 + 5.2.2 24 0 @@ -185,7 +185,7 @@ org.jacoco jacoco-maven-plugin - 0.8.3 + 0.8.5 prepare-agent @@ -217,7 +217,7 @@ maven-source-plugin - 3.0.1 + 3.1.0 attach-sources @@ -229,7 +229,7 @@ maven-javadoc-plugin - 3.0.1 + 3.1.1 attach-javadocs From 8165adcff6e438047885ba826cd3ea3753989601 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 13:58:53 +0100 Subject: [PATCH 50/62] tuned unit tests --- .../cryptofs/dir/C9rDecryptorTest.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java index 6ddfd320..5fa557ba 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java @@ -26,14 +26,6 @@ public void setup() { cryptor = Mockito.mock(Cryptor.class); fileNameCryptor = Mockito.mock(FileNameCryptor.class); Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); - Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).then(invocation -> { - String ciphertext = invocation.getArgument(1); - if (ciphertext.equals("aaaaBBBBccccDDDDeeeeFFFF")) { - return "FFFFeeeeDDDDccccBBBBaaaa"; - } else { - throw new AuthenticationFailedException("Invalid ciphertext " + ciphertext); - } - }); decryptor = new C9rDecryptor(cryptor, "foo"); } @@ -67,33 +59,44 @@ public void testInvalidBase64Pattern(String input) { @Test @DisplayName("process canonical filename") public void testProcessFullMatch() { + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("helloWorld.txt"); NodeNames input = new NodeNames(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); Stream resultStream = decryptor.process(input); Optional optionalResult = resultStream.findAny(); Assertions.assertTrue(optionalResult.isPresent()); - Assertions.assertEquals("FFFFeeeeDDDDccccBBBBaaaa", optionalResult.get().cleartextName); + Assertions.assertEquals("helloWorld.txt", optionalResult.get().cleartextName); } @DisplayName("process non-canonical filename") @ParameterizedTest(name = "{0}") @ValueSource(strings = { - "aaaaBBBBccccDDDDeeeeFFFF (conflict3000).c9r", - "(conflict3000) aaaaBBBBccccDDDDeeeeFFFF.c9r", - "conflict_aaaaBBBBccccDDDDeeeeFFFF.c9r", - "aaaaBBBBccccDDDDeeeeFFFF_conflict.c9r", - "_aaaaBBBBccccDDDDeeeeFFFF.c9r", - "aaaaBBBBccccDDDDeeeeFFFF_.c9r", + "aaaaBBBBcccc_--_11112222 (conflict3000).c9r", + "(conflict3000) aaaaBBBBcccc_--_11112222.c9r", + "conflict_aaaaBBBBcccc_--_11112222.c9r", + "aaaaBBBBcccc_--_11112222_conflict.c9r", + "____aaaaBBBBcccc_--_11112222.c9r", + "aaaaBBBBcccc_--_11112222____.c9r", + "foo_aaaaBBBBcccc_--_11112222_foo.c9r", + "aaaaBBBBccccDDDDeeeeFFFF___aaaaBBBBcccc_--_11112222----aaaaBBBBccccDDDDeeeeFFFF.c9r", }) public void testProcessPartialMatch(String filename) { + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).then(invocation -> { + String ciphertext = invocation.getArgument(1); + if (ciphertext.equals("aaaaBBBBcccc_--_11112222")) { + return "helloWorld.txt"; + } else { + throw new AuthenticationFailedException("Invalid ciphertext " + ciphertext); + } + }); NodeNames input = new NodeNames(Paths.get(filename)); Stream resultStream = decryptor.process(input); Optional optionalResult = resultStream.findAny(); Assertions.assertTrue(optionalResult.isPresent()); - Assertions.assertEquals("FFFFeeeeDDDDccccBBBBaaaa", optionalResult.get().cleartextName); + Assertions.assertEquals("helloWorld.txt", optionalResult.get().cleartextName); } @DisplayName("process filename without ciphertext") @@ -105,6 +108,7 @@ public void testProcessPartialMatch(String filename) { "aaaaBBBBxxxxDDDDeeeeFFFF.c9r", }) public void testProcessNoMatch(String filename) { + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new AuthenticationFailedException("Invalid ciphertext.")); NodeNames input = new NodeNames(Paths.get(filename)); Stream resultStream = decryptor.process(input); From 24331779558764ff114db9b380172ea4b4ea39b0 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 14:09:28 +0100 Subject: [PATCH 51/62] Use some better names for classes and variables --- .../cryptofs/dir/C9rConflictResolver.java | 44 +++++++++---------- .../cryptofs/dir/C9rDecryptor.java | 30 ++++++------- .../cryptofs/dir/C9rProcessor.java | 4 +- .../cryptofs/dir/CryptoDirectoryStream.java | 6 +-- .../dir/{NodeNames.java => Node.java} | 10 ++--- .../cryptofs/dir/NodeProcessor.java | 8 ++-- .../cryptofs/dir/C9rDecryptorTest.java | 18 ++++---- .../dir/CryptoDirectoryStreamTest.java | 30 ++++++------- 8 files changed, 74 insertions(+), 76 deletions(-) rename src/main/java/org/cryptomator/cryptofs/dir/{NodeNames.java => Node.java} (53%) diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java index 9bdf210a..60ae067a 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -4,9 +4,7 @@ import com.google.common.io.BaseEncoding; import com.google.common.io.MoreFiles; import com.google.common.io.RecursiveDeleteOption; -import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,33 +43,33 @@ public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId) { this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); } - public Stream process(NodeNames nodeNames) { - Preconditions.checkArgument(nodeNames.ciphertextName != null, "Can only resolve conflicts if ciphertextName is set"); - Preconditions.checkArgument(nodeNames.cleartextName != null, "Can only resolve conflicts if cleartextName is set"); + public Stream process(Node node) { + Preconditions.checkArgument(node.extractedCiphertext != null, "Can only resolve conflicts if extractedCiphertext is set"); + Preconditions.checkArgument(node.cleartextName != null, "Can only resolve conflicts if cleartextName is set"); - String canonicalName = nodeNames.ciphertextName + Constants.CRYPTOMATOR_FILE_SUFFIX; - if (nodeNames.ciphertextFileName.equals(canonicalName)) { - return Stream.of(nodeNames); + String canonicalCiphertextFileName = node.extractedCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX; + if (node.fullCiphertextFileName.equals(canonicalCiphertextFileName)) { + return Stream.of(node); } else { try { - Path canonicalPath = nodeNames.ciphertextPath.resolveSibling(canonicalName); - return resolveConflict(nodeNames, canonicalPath); + Path canonicalPath = node.ciphertextPath.resolveSibling(canonicalCiphertextFileName); + return resolveConflict(node, canonicalPath); } catch (IOException e) { - LOG.error("Failed to resolve conflict for " + nodeNames.ciphertextPath, e); + LOG.error("Failed to resolve conflict for " + node.ciphertextPath, e); return Stream.empty(); } } } - private Stream resolveConflict(NodeNames nodeNames, Path canonicalPath) throws IOException { - Path conflictingPath = nodeNames.ciphertextPath; + private Stream resolveConflict(Node conflicting, Path canonicalPath) throws IOException { + Path conflictingPath = conflicting.ciphertextPath; if (resolveConflictTrivially(canonicalPath, conflictingPath)) { - NodeNames resolvedNames = new NodeNames(canonicalPath); - resolvedNames.cleartextName = nodeNames.cleartextName; - resolvedNames.ciphertextName = nodeNames.ciphertextName; - return Stream.of(resolvedNames); + Node resolved = new Node(canonicalPath); + resolved.cleartextName = conflicting.cleartextName; + resolved.extractedCiphertext = conflicting.extractedCiphertext; + return Stream.of(resolved); } else { - return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, nodeNames.cleartextName)); + return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, conflicting.cleartextName)); } } @@ -84,7 +82,7 @@ private Stream resolveConflict(NodeNames nodeNames, Path canonicalPat * @return The NodeNames for the newly created node after renaming the conflicting file. * @throws IOException */ - private NodeNames renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException { + private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException { assert Files.exists(canonicalPath); final int beginOfFileExtension = cleartext.lastIndexOf('.'); final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : ""; @@ -104,10 +102,10 @@ private NodeNames renameConflictingFile(Path canonicalPath, Path conflictingPath assert alternativeCiphertextName.length() <= MAX_CIPHERTEXT_NAME_LENGTH; LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); - NodeNames nodeNames = new NodeNames(alternativePath); - nodeNames.cleartextName = alternativeCleartext; - nodeNames.ciphertextName = alternativeCiphertext; - return nodeNames; + Node node = new Node(alternativePath); + node.cleartextName = alternativeCleartext; + node.extractedCiphertext = alternativeCiphertext; + return node; } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java index 90c65b12..7eb0b594 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java @@ -32,10 +32,10 @@ public C9rDecryptor(Cryptor cryptor, @Named("dirId") String dirId) { this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); } - public Stream process(NodeNames nodeNames) { - String basename = StringUtils.removeEnd(nodeNames.ciphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX); + public Stream process(Node node) { + String basename = StringUtils.removeEnd(node.fullCiphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX); Matcher matcher = BASE64_PATTERN.matcher(basename); - Optional match = extractCiphertext(nodeNames, matcher, 0, basename.length()); + Optional match = extractCiphertext(node, matcher, 0, basename.length()); if (match.isPresent()) { return Stream.of(match.get()); } else { @@ -43,17 +43,17 @@ public Stream process(NodeNames nodeNames) { } } - private Optional extractCiphertext(NodeNames nodeNames, Matcher matcher, int start, int end) { + private Optional extractCiphertext(Node node, Matcher matcher, int start, int end) { matcher.region(start, end); if (matcher.find()) { - final MatchResult matchResult = matcher.toMatchResult(); - final String validBase64 = matchResult.group(); + final MatchResult match = matcher.toMatchResult(); + final String validBase64 = match.group(); assert validBase64.length() >= 24; - assert matchResult.end() - matchResult.start() >= 24; + assert match.end() - match.start() >= 24; try { - nodeNames.cleartextName = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), validBase64, dirId); - nodeNames.ciphertextName = validBase64; - return Optional.of(nodeNames); + node.cleartextName = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), validBase64, dirId); + node.extractedCiphertext = validBase64; + return Optional.of(node); } catch (AuthenticationFailedException e) { // narrow down to sub-base64-sequences: int firstDelimIdx = DELIM_MATCHER.indexIn(validBase64); @@ -64,18 +64,18 @@ private Optional extractCiphertext(NodeNames nodeNames, Matcher match } assert firstDelimIdx != -1; assert lastDelimIdx != -1; - Optional subsequenceMatch = Optional.empty(); + Optional subsequenceMatch = Optional.empty(); if (!subsequenceMatch.isPresent() && firstDelimIdx == 0) { - subsequenceMatch = extractCiphertext(nodeNames, matcher, matchResult.start() + 1, end); + subsequenceMatch = extractCiphertext(node, matcher, match.start() + 1, end); } if (!subsequenceMatch.isPresent() && lastDelimIdx == validBase64.length() - 1) { - subsequenceMatch = extractCiphertext(nodeNames, matcher, start, matchResult.end() - 1); + subsequenceMatch = extractCiphertext(node, matcher, start, match.end() - 1); } if (!subsequenceMatch.isPresent() && firstDelimIdx > 0) { - subsequenceMatch = extractCiphertext(nodeNames, matcher, matchResult.start() + firstDelimIdx, end); + subsequenceMatch = extractCiphertext(node, matcher, match.start() + firstDelimIdx, end); } if (!subsequenceMatch.isPresent() && lastDelimIdx < validBase64.length() - 1) { - subsequenceMatch = extractCiphertext(nodeNames, matcher, start, matchResult.start() + lastDelimIdx); + subsequenceMatch = extractCiphertext(node, matcher, start, match.start() + lastDelimIdx); } return subsequenceMatch; } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java index 7e613fa8..d3d9adba 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java @@ -15,8 +15,8 @@ public C9rProcessor(C9rDecryptor decryptor, C9rConflictResolver conflictResolver this.conflictResolver = conflictResolver; } - public Stream process(NodeNames nodeNames) { - return decryptor.process(nodeNames).flatMap(conflictResolver::process); + public Stream process(Node node) { + return decryptor.process(node).flatMap(conflictResolver::process); } } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index 13939ac9..9171174e 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -56,12 +56,12 @@ private Stream cleartextDirectoryListing() { .filter(this::isAcceptableByFilter); } - public Stream ciphertextDirectoryListing() { + Stream ciphertextDirectoryListing() { return directoryListing().map(node -> node.ciphertextPath); } - private Stream directoryListing() { - return StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(NodeNames::new).flatMap(nodeProcessor::process); + private Stream directoryListing() { + return StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(Node::new).flatMap(nodeProcessor::process); // Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); // return sanitized; } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java b/src/main/java/org/cryptomator/cryptofs/dir/Node.java similarity index 53% rename from src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java rename to src/main/java/org/cryptomator/cryptofs/dir/Node.java index 2fc15021..aa12df1f 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/NodeNames.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/Node.java @@ -3,16 +3,16 @@ import java.nio.file.Path; import java.util.Objects; -class NodeNames { +class Node { public final Path ciphertextPath; - public final String ciphertextFileName; - public String ciphertextName; + public final String fullCiphertextFileName; + public String extractedCiphertext; public String cleartextName; - public NodeNames(Path ciphertextPath) { + public Node(Path ciphertextPath) { this.ciphertextPath = Objects.requireNonNull(ciphertextPath); - this.ciphertextFileName = ciphertextPath.getFileName().toString(); + this.fullCiphertextFileName = ciphertextPath.getFileName().toString(); } } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java index a67af665..f99da692 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java @@ -15,10 +15,10 @@ public NodeProcessor(C9rProcessor c9rProcessor){ this.c9rProcessor = c9rProcessor; } - public Stream process(NodeNames nodeNames) { - if (nodeNames.ciphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX)) { - return c9rProcessor.process(nodeNames); - } else if (nodeNames.ciphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX)) { + public Stream process(Node node) { + if (node.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX)) { + return c9rProcessor.process(node); + } else if (node.fullCiphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX)) { return Stream.empty(); } else { return Stream.empty(); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java index 5fa557ba..1d214212 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java @@ -60,10 +60,10 @@ public void testInvalidBase64Pattern(String input) { @DisplayName("process canonical filename") public void testProcessFullMatch() { Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("helloWorld.txt"); - NodeNames input = new NodeNames(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); + Node input = new Node(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); - Stream resultStream = decryptor.process(input); - Optional optionalResult = resultStream.findAny(); + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); Assertions.assertTrue(optionalResult.isPresent()); Assertions.assertEquals("helloWorld.txt", optionalResult.get().cleartextName); @@ -90,10 +90,10 @@ public void testProcessPartialMatch(String filename) { throw new AuthenticationFailedException("Invalid ciphertext " + ciphertext); } }); - NodeNames input = new NodeNames(Paths.get(filename)); + Node input = new Node(Paths.get(filename)); - Stream resultStream = decryptor.process(input); - Optional optionalResult = resultStream.findAny(); + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); Assertions.assertTrue(optionalResult.isPresent()); Assertions.assertEquals("helloWorld.txt", optionalResult.get().cleartextName); @@ -109,10 +109,10 @@ public void testProcessPartialMatch(String filename) { }) public void testProcessNoMatch(String filename) { Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new AuthenticationFailedException("Invalid ciphertext.")); - NodeNames input = new NodeNames(Paths.get(filename)); + Node input = new Node(Paths.get(filename)); - Stream resultStream = decryptor.process(input); - Optional optionalResult = resultStream.findAny(); + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); Assertions.assertFalse(optionalResult.isPresent()); } diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index de2adf7c..a6ef6540 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -42,8 +42,8 @@ public void setup() throws IOException { dirStream = Mockito.mock(DirectoryStream.class); } - private ArgumentMatcher nodeNamed(String name) { - return node -> node.ciphertextFileName.equals(name); + private ArgumentMatcher nodeNamed(String name) { + return node -> node.fullCiphertextFileName.equals(name); } @Test @@ -57,23 +57,23 @@ public void testDirListing() throws IOException { ciphertextFileNames.add("invalidCiphertext"); Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(ciphertextPath::resolve).spliterator()); Mockito.doAnswer(invocation -> { - NodeNames nodeNames = invocation.getArgument(0); - nodeNames.cleartextName = "cleartextFile1"; - return Stream.of(nodeNames); - }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextFile1"))); + Node node = invocation.getArgument(0); + node.cleartextName = "cleartextFile1"; + return Stream.of(node); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.fullCiphertextFileName.equals("ciphertextFile1"))); Mockito.doAnswer(invocation -> { - NodeNames nodeNames = invocation.getArgument(0); - nodeNames.cleartextName = "cleartextFile2"; - return Stream.of(nodeNames); - }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextFile2"))); + Node node = invocation.getArgument(0); + node.cleartextName = "cleartextFile2"; + return Stream.of(node); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.fullCiphertextFileName.equals("ciphertextFile2"))); Mockito.doAnswer(invocation -> { - NodeNames nodeNames = invocation.getArgument(0); - nodeNames.cleartextName = "cleartextDirectory1"; - return Stream.of(nodeNames); - }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("ciphertextDirectory1"))); + Node node = invocation.getArgument(0); + node.cleartextName = "cleartextDirectory1"; + return Stream.of(node); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.fullCiphertextFileName.equals("ciphertextDirectory1"))); Mockito.doAnswer(invocation -> { return Stream.empty(); - }).when(nodeProcessor).process(Mockito.argThat(node -> node.ciphertextFileName.equals("invalidCiphertext"))); + }).when(nodeProcessor).process(Mockito.argThat(node -> node.fullCiphertextFileName.equals("invalidCiphertext"))); try (CryptoDirectoryStream stream = new CryptoDirectoryStream("foo", dirStream, cleartextPath, ACCEPT_ALL, DO_NOTHING_ON_CLOSE, nodeProcessor)) { Iterator iter = stream.iterator(); From 3a27a73f10762764e1252a96b81fb6d5cf30340c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 14:45:03 +0100 Subject: [PATCH 52/62] now only two instead of four cases leading to recursion --- .../cryptofs/dir/C9rDecryptor.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java index 7eb0b594..e3bf1fda 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java @@ -58,30 +58,34 @@ private Optional extractCiphertext(Node node, Matcher matcher, int start, // narrow down to sub-base64-sequences: int firstDelimIdx = DELIM_MATCHER.indexIn(validBase64); int lastDelimIdx = DELIM_MATCHER.lastIndexIn(validBase64); + + // fail fast if there is no way to find a different subsequence: if (firstDelimIdx == -1) { assert lastDelimIdx == -1; return Optional.empty(); } - assert firstDelimIdx != -1; - assert lastDelimIdx != -1; - Optional subsequenceMatch = Optional.empty(); - if (!subsequenceMatch.isPresent() && firstDelimIdx == 0) { - subsequenceMatch = extractCiphertext(node, matcher, match.start() + 1, end); - } - if (!subsequenceMatch.isPresent() && lastDelimIdx == validBase64.length() - 1) { - subsequenceMatch = extractCiphertext(node, matcher, start, match.end() - 1); - } - if (!subsequenceMatch.isPresent() && firstDelimIdx > 0) { - subsequenceMatch = extractCiphertext(node, matcher, match.start() + firstDelimIdx, end); + + // try matching with adjusted start and same end: + int newStart = match.start() + Math.max(1, firstDelimIdx); + assert match.start() >= start; + assert newStart > start; + Optional matchWithNewStart = extractCiphertext(node, matcher, newStart, end); + if (matchWithNewStart.isPresent()) { + return matchWithNewStart; } - if (!subsequenceMatch.isPresent() && lastDelimIdx < validBase64.length() - 1) { - subsequenceMatch = extractCiphertext(node, matcher, start, match.start() + lastDelimIdx); + + // try matching with same start and adjusted end: + int delimDistanceFromEnd = validBase64.length() - lastDelimIdx; + int newEnd = match.end() - Math.max(1, delimDistanceFromEnd); + assert match.end() <= end; + assert newEnd < end; + Optional matchWithNewEnd = extractCiphertext(node, matcher, start, newEnd); + if (matchWithNewEnd.isPresent()) { + return matchWithNewEnd; } - return subsequenceMatch; } - } else { - return Optional.empty(); } + return Optional.empty(); } } From de39a317b0da4553df0e4852e9646ef1092109c1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 15:18:21 +0100 Subject: [PATCH 53/62] added unit tests for c9r conflict resolver --- .../cryptofs/dir/C9rConflictResolverTest.java | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java new file mode 100644 index 00000000..a983fc14 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java @@ -0,0 +1,116 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Stream; + +class C9rConflictResolverTest { + + private Cryptor cryptor; + private FileNameCryptor fileNameCryptor; + private C9rConflictResolver conflictResolver; + + @BeforeEach + public void setup() { + cryptor = Mockito.mock(Cryptor.class); + fileNameCryptor = Mockito.mock(FileNameCryptor.class); + Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + conflictResolver = new C9rConflictResolver(cryptor, "foo"); + } + + @Test + public void testResolveNonConflictingNode() { + Node unresolved = new Node(Paths.get("foo.c9r")); + unresolved.cleartextName = "bar"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertSame(unresolved, resolved); + } + + @Test + public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throws IOException { + Files.createFile(dir.resolve("foo (1).c9r")); + Files.createFile(dir.resolve("foo.c9r")); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz"); + Node unresolved = new Node(dir.resolve("foo (1).c9r")); + unresolved.cleartextName = "bar.txt"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertNotEquals(unresolved, resolved); + Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName); + Assertions.assertEquals("bar (1).txt", resolved.cleartextName); + Assertions.assertTrue(Files.exists(resolved.ciphertextPath)); + Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); + } + + @Test + public void testResolveConflictingFileTrivially(@TempDir Path dir) throws IOException { + Files.createFile(dir.resolve("foo (1).c9r")); + Node unresolved = new Node(dir.resolve("foo (1).c9r")); + unresolved.cleartextName = "bar"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertNotEquals(unresolved, resolved); + Assertions.assertEquals("foo.c9r", resolved.fullCiphertextFileName); + Assertions.assertTrue(Files.exists(resolved.ciphertextPath)); + Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); + } + + @Test + public void testResolveConflictingDirTrivially(@TempDir Path dir) throws IOException { + Files.createDirectory(dir.resolve("foo (1).c9r")); + Files.createDirectory(dir.resolve("foo.c9r")); + Files.write(dir.resolve("foo (1).c9r/dir.c9r"), "dirid".getBytes()); + Files.write(dir.resolve("foo.c9r/dir.c9r"), "dirid".getBytes()); + Node unresolved = new Node(dir.resolve("foo (1).c9r")); + unresolved.cleartextName = "bar"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertNotEquals(unresolved, resolved); + Assertions.assertEquals("foo.c9r", resolved.fullCiphertextFileName); + Assertions.assertTrue(Files.exists(resolved.ciphertextPath)); + Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); + } + + @Test + public void testResolveConflictingSymlinkTrivially(@TempDir Path dir) throws IOException { + Files.createDirectory(dir.resolve("foo (1).c9r")); + Files.createDirectory(dir.resolve("foo.c9r")); + Files.write(dir.resolve("foo (1).c9r/symlink.c9r"), "linktarget".getBytes()); + Files.write(dir.resolve("foo.c9r/symlink.c9r"), "linktarget".getBytes()); + Node unresolved = new Node(dir.resolve("foo (1).c9r")); + unresolved.cleartextName = "bar"; + unresolved.extractedCiphertext = "foo"; + + Stream result = conflictResolver.process(unresolved); + Node resolved = result.findAny().get(); + + Assertions.assertNotEquals(unresolved, resolved); + Assertions.assertEquals("foo.c9r", resolved.fullCiphertextFileName); + Assertions.assertTrue(Files.exists(resolved.ciphertextPath)); + Assertions.assertFalse(Files.exists(unresolved.ciphertextPath)); + } + +} \ No newline at end of file From bd088c4f3f4ea8a2dfe902941b28141a8b81ed62 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 16:04:14 +0100 Subject: [PATCH 54/62] restored ability to remove broken dir files from dir listing --- .../cryptofs/dir/BrokenDirectoryFilter.java | 45 +++++++++++++++ .../cryptofs/dir/CryptoDirectoryStream.java | 30 ---------- .../cryptofs/dir/NodeProcessor.java | 6 +- .../dir/BrokenDirectoryFilterTest.java | 55 +++++++++++++++++++ 4 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java create mode 100644 src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java new file mode 100644 index 00000000..2c05b34b --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java @@ -0,0 +1,45 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.common.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class BrokenDirectoryFilter { + + private static final Logger LOG = LoggerFactory.getLogger(BrokenDirectoryFilter.class); + + private final CryptoPathMapper cryptoPathMapper; + + @Inject + public BrokenDirectoryFilter(CryptoPathMapper cryptoPathMapper) { + this.cryptoPathMapper = cryptoPathMapper; + } + + public Stream process(Node node) { + Path dirFile = node.ciphertextPath.resolve(Constants.DIR_FILE_NAME); + if (Files.isRegularFile(dirFile)) { + final Path dirPath; + try { + dirPath = cryptoPathMapper.resolveDirectory(dirFile).path; + } catch (IOException e) { + LOG.warn("Broken directory file: " + dirFile, e); + return Stream.empty(); + } + if (!Files.isDirectory(dirPath)) { + LOG.warn("Broken directory file {}. Directory {} does not exist.", dirFile, dirPath); + return Stream.empty(); + } + } + return Stream.of(node); + } + + +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java index 9171174e..da186334 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -62,38 +62,8 @@ Stream ciphertextDirectoryListing() { private Stream directoryListing() { return StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(Node::new).flatMap(nodeProcessor::process); -// Stream sanitized = decrypted.filter(this::passesPlausibilityChecks); -// return sanitized; } -// /** -// * Checks if a given file belongs into this ciphertext dir. -// * -// * @param paths The path to check. -// * @return true if the file is an existing ciphertext or directory file. -// */ -// private boolean passesPlausibilityChecks(NodeNames paths) { -// return !isBrokenDirectoryFile(paths); -// } -// -// private boolean isBrokenDirectoryFile(NodeNames paths) { -// Path potentialDirectoryFile = paths.getCiphertextPath().resolve(Constants.DIR_FILE_NAME); -// if (Files.isRegularFile(potentialDirectoryFile)) { -// final Path dirPath; -// try { -// dirPath = cryptoPathMapper.resolveDirectory(potentialDirectoryFile).path; -// } catch (IOException e) { -// LOG.warn("Broken directory file {}. Exception: {}", potentialDirectoryFile, e.getMessage()); -// return true; -// } -// if (!Files.isDirectory(dirPath)) { -// LOG.warn("Broken directory file {}. Directory {} does not exist.", potentialDirectoryFile, dirPath); -// return true; -// } -// } -// return false; -// } - private boolean isAcceptableByFilter(Path path) { try { return filter.accept(path); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java index f99da692..5afa6bff 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java @@ -9,15 +9,17 @@ class NodeProcessor { private final C9rProcessor c9rProcessor; + private final BrokenDirectoryFilter brokenDirFilter; @Inject - public NodeProcessor(C9rProcessor c9rProcessor){ + public NodeProcessor(C9rProcessor c9rProcessor, BrokenDirectoryFilter brokenDirFilter){ this.c9rProcessor = c9rProcessor; + this.brokenDirFilter = brokenDirFilter; } public Stream process(Node node) { if (node.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX)) { - return c9rProcessor.process(node); + return c9rProcessor.process(node).flatMap(brokenDirFilter::process); } else if (node.fullCiphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX)) { return Stream.empty(); } else { diff --git a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java new file mode 100644 index 00000000..1741b73b --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java @@ -0,0 +1,55 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +class BrokenDirectoryFilterTest { + + private CryptoPathMapper cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); + private BrokenDirectoryFilter brokenDirectoryFilter = new BrokenDirectoryFilter(cryptoPathMapper); + + @Test + public void testProcessNonDirectoryNode(@TempDir Path dir) { + Node unfiltered = new Node(dir.resolve("foo.c9r")); + + Stream result = brokenDirectoryFilter.process(unfiltered); + Node filtered = result.findAny().get(); + + Assertions.assertSame(unfiltered, filtered); + } + + @Test + public void testProcessNormalDirectoryNode(@TempDir Path dir) throws IOException { + Path targetDir = Files.createDirectories(dir.resolve("d/ab/cdefg")); + Files.createDirectory(dir.resolve("foo.c9r")); + Files.write(dir.resolve("foo.c9r/dir.c9r"), "".getBytes()); + Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CryptoPathMapper.CiphertextDirectory("asd", targetDir)); + Node unfiltered = new Node(dir.resolve("foo.c9r")); + + Stream result = brokenDirectoryFilter.process(unfiltered); + Node filtered = result.findAny().get(); + + Assertions.assertSame(unfiltered, filtered); + } + + @Test + public void testProcessNodeWithMissingTargetDir(@TempDir Path dir) throws IOException { + Path targetDir = dir.resolve("d/ab/cdefg"); // not existing! + Files.createDirectory(dir.resolve("foo.c9r")); + Files.write(dir.resolve("foo.c9r/dir.c9r"), "".getBytes()); + Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CryptoPathMapper.CiphertextDirectory("asd", targetDir)); + Node unfiltered = new Node(dir.resolve("foo.c9r")); + + Stream result = brokenDirectoryFilter.process(unfiltered); + Assertions.assertFalse(result.findAny().isPresent()); + } + +} \ No newline at end of file From 67bae9c0799dff3d899480b84b2e5f4a30a25259 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 16:09:57 +0100 Subject: [PATCH 55/62] Remove unused code --- .../cryptofs/dir/ConflictResolver.java | 236 ------------------ .../cryptofs/dir/DirectoryStreamFactory.java | 2 +- .../cryptofs/dir/DirectoryStreamScoped.java | 2 +- .../cryptofs/dir/ConflictResolverTest.java | 170 ------------- .../CryptoDirectoryStreamIntegrationTest.java | 62 ----- .../dir/CryptoDirectoryStreamTest.java | 4 - 6 files changed, 2 insertions(+), 474 deletions(-) delete mode 100644 src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java delete mode 100644 src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java delete mode 100644 src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java deleted file mode 100644 index 58eff2ac..00000000 --- a/src/main/java/org/cryptomator/cryptofs/dir/ConflictResolver.java +++ /dev/null @@ -1,236 +0,0 @@ -package org.cryptomator.cryptofs.dir; - -import com.google.common.io.BaseEncoding; -import com.google.common.io.MoreFiles; -import com.google.common.io.RecursiveDeleteOption; -import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CryptoFileSystemScoped; -import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.StringUtils; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.Cryptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static org.cryptomator.cryptofs.common.Constants.CRYPTOMATOR_FILE_SUFFIX; -import static org.cryptomator.cryptofs.common.Constants.DEFLATED_FILE_SUFFIX; -import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME; -import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH; -import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME; - -@Deprecated -@CryptoFileSystemScoped -class ConflictResolver { - - private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class); - private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}"); - private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes - - private final LongFileNameProvider longFileNameProvider; - private final CryptoPathMapper cryptoPathMapper; - private final Cryptor cryptor; - - @Inject - public ConflictResolver(LongFileNameProvider longFileNameProvider, CryptoPathMapper cryptoPathMapper, Cryptor cryptor) { - this.longFileNameProvider = longFileNameProvider; - this.cryptoPathMapper = cryptoPathMapper; - this.cryptor = cryptor; - } - - /** - * Checks if the name of the file represented by the given ciphertextPath is a valid ciphertext name without any additional chars. - * If any unexpected chars are found on the name but it still contains an authentic ciphertext, it is considered a conflicting file. - * Conflicting files will be given a new name. The caller must use the path returned by this function after invoking it, as the given ciphertextPath might be no longer valid. - * - * @param ciphertextPath The path to a file to check. - * @param dirId The directory id of the file's parent directory. - * @return Either the original name if no unexpected chars have been found or a completely new path. - * @throws IOException - */ - public Path resolveConflictsIfNecessary(Path ciphertextPath, String dirId) throws IOException { - String ciphertextFileName = ciphertextPath.getFileName().toString(); - - final String basename; - final String extension; - if (ciphertextFileName.endsWith(DEFLATED_FILE_SUFFIX)) { - basename = StringUtils.removeEnd(ciphertextFileName, DEFLATED_FILE_SUFFIX); - extension = DEFLATED_FILE_SUFFIX; - } else if (ciphertextFileName.endsWith(CRYPTOMATOR_FILE_SUFFIX)) { - basename = StringUtils.removeEnd(ciphertextFileName, CRYPTOMATOR_FILE_SUFFIX); - extension = CRYPTOMATOR_FILE_SUFFIX; - } else { - // file doesn't belong to the vault structure -> nothing to resolve - return ciphertextPath; - } - - Matcher m = BASE64_PATTERN.matcher(basename); - if (!m.matches() && m.find(0)) { - // no full match, but still contains base64 -> partial match - Path canonicalPath = ciphertextPath.resolveSibling(m.group() + extension); - return resolveConflict(ciphertextPath, canonicalPath, dirId); - } else { - // full match or no match at all -> nothing to resolve - return ciphertextPath; - } - } - - //private static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}"); - //private static final Pattern DELIM_PATTERN = Pattern.compile("[!a-zA-Z0-9]"); -// public String extractCiphertext(String potentialCiphertext, byte[] dirIdBytes) { -// // attempt a full match: -// Matcher m = BASE64_PATTERN.matcher(potentialCiphertext); -// if (m.matches()) { -// try { -// String cleartext = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), potentialCiphertext, dirIdBytes); -// return potentialCiphertext; -// } catch (AuthenticationFailedException e) { -// // proceed with partial matches -// } -// } -// -// // strip potential prefix: -// Matcher d = DELIM_PATTERN.matcher(potentialCiphertext); -// if (d.find()) { -// int endOfPrefix = d.end(); -// assert endOfPrefix > 0; -// return extractCiphertext(potentialCiphertext.substring(endOfPrefix), dirIdBytes); -// } -// -// // strip potential suffix: -// int beginOfSuffix = 0; -// while (d.find(beginOfSuffix)) { // we can only loop through matches from begin to end to find the last match -// beginOfSuffix = d.start(); -// } -// if (beginOfSuffix > 0) { -// assert beginOfSuffix < potentialCiphertext.length() - 1; -// return extractCiphertext(potentialCiphertext.substring(0, beginOfSuffix), dirIdBytes); -// } -// -// // no match: -// return null; -// } - - /** - * Resolves a conflict. - * - * @param conflictingPath The path to the potentially conflicting file. - * @param canonicalPath The path to the original (conflict-free) file. - * @param dirId The directory id of the file's parent directory. - * @return The new path of the conflicting file after the conflict has been resolved. - * @throws IOException - */ - private Path resolveConflict(Path conflictingPath, Path canonicalPath, String dirId) throws IOException { - if (resolveConflictTrivially(canonicalPath, conflictingPath)) { - return canonicalPath; - } - - // get ciphertext part from file: - String canonicalFileName = canonicalPath.getFileName().toString(); - String ciphertext; - if (longFileNameProvider.isDeflated(canonicalFileName)) { - String inflatedFileName = longFileNameProvider.inflate(canonicalPath); - ciphertext = StringUtils.removeEnd(inflatedFileName, CRYPTOMATOR_FILE_SUFFIX); - } else { - ciphertext = StringUtils.removeEnd(canonicalFileName, CRYPTOMATOR_FILE_SUFFIX); - } - - return renameConflictingFile(canonicalPath, conflictingPath, ciphertext, dirId); - } - - /** - * Resolves a conflict by renaming the conflicting file. - * - * @param canonicalPath The path to the original (conflict-free) file. - * @param conflictingPath The path to the potentially conflicting file. - * @param ciphertext The (previously inflated) ciphertext name of the file without any preceeding directory prefix. - * @param dirId The directory id of the file's parent directory. - * @return The new path after renaming the conflicting file. - * @throws IOException - */ - private Path renameConflictingFile(Path canonicalPath, Path conflictingPath, String ciphertext, String dirId) throws IOException { - assert Files.exists(canonicalPath); - Path ciphertextParentDir = canonicalPath.getParent(); - try { - String cleartext = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId.getBytes(StandardCharsets.UTF_8)); - CiphertextFilePath alternativePath; - int i = 1; - do { - String alternativeCleartext = cleartext + " (Conflict " + i++ + ")"; - alternativePath = cryptoPathMapper.getCiphertextFilePath(ciphertextParentDir, dirId, alternativeCleartext); - } while(Files.exists(alternativePath.getRawPath())); - LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath.getRawPath()); - Path resolved = Files.move(conflictingPath, alternativePath.getRawPath(), StandardCopyOption.ATOMIC_MOVE); - alternativePath.persistLongFileName(); - return resolved; - } catch (AuthenticationFailedException e) { - // not decryptable, no need to resolve any kind of conflict - LOG.info("Found valid Base64 string, which is an unauthentic ciphertext: {}", conflictingPath); - return conflictingPath; - } - } - - /** - * Tries to resolve a conflicting file without renaming the file. If successful, only the file with the canonical path will exist afterwards. - * - * @param canonicalPath The path to the original (conflict-free) resource (must not exist). - * @param conflictingPath The path to the potentially conflicting file (known to exist). - * @return true if the conflict has been resolved. - * @throws IOException - */ - private boolean resolveConflictTrivially(Path canonicalPath, Path conflictingPath) throws IOException { - if (!Files.exists(canonicalPath)) { - Files.move(conflictingPath, canonicalPath); // boom. conflict solved. - return true; - } else if (hasSameFileContent(conflictingPath.resolve(DIR_FILE_NAME), canonicalPath.resolve(DIR_FILE_NAME), MAX_DIR_FILE_SIZE)) { - LOG.info("Removing conflicting directory {} (identical to {})", conflictingPath, canonicalPath); - MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); - return true; - } else if (hasSameFileContent(conflictingPath.resolve(SYMLINK_FILE_NAME), canonicalPath.resolve(SYMLINK_FILE_NAME), MAX_SYMLINK_LENGTH)) { - LOG.info("Removing conflicting symlink {} (identical to {})", conflictingPath, canonicalPath); - MoreFiles.deleteRecursively(conflictingPath, RecursiveDeleteOption.ALLOW_INSECURE); - return true; - } else { - return false; - } - } - - /** - * @param conflictingPath Path to a potentially conflicting file supposedly containing a directory id - * @param canonicalPath Path to the canonical file containing a directory id - * @param numBytesToCompare Number of bytes to read from each file and compare to each other. - * @return true if the first {@value #MAX_DIR_FILE_SIZE} bytes are equal in both files. - * @throws IOException If an I/O exception occurs while reading either file. - */ - private boolean hasSameFileContent(Path conflictingPath, Path canonicalPath, int numBytesToCompare) throws IOException { - if (!Files.isDirectory(conflictingPath.getParent()) || !Files.isDirectory(canonicalPath.getParent())) { - return false; - } - try (ReadableByteChannel in1 = Files.newByteChannel(conflictingPath, StandardOpenOption.READ); // - ReadableByteChannel in2 = Files.newByteChannel(canonicalPath, StandardOpenOption.READ)) { - ByteBuffer buf1 = ByteBuffer.allocate(numBytesToCompare); - ByteBuffer buf2 = ByteBuffer.allocate(numBytesToCompare); - int read1 = in1.read(buf1); - int read2 = in2.read(buf2); - buf1.flip(); - buf2.flip(); - return read1 == read2 && buf1.compareTo(buf2) == 0; - } catch (NoSuchFileException e) { - return false; - } - } - -} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java index 5a5556d7..bcacb283 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -18,7 +18,7 @@ @CryptoFileSystemScoped public class DirectoryStreamFactory { - + private final CryptoPathMapper cryptoPathMapper; private final DirectoryStreamComponent.Builder directoryStreamComponentBuilder; private final Map streams = new HashMap<>(); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java index a7103d27..35dfba07 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamScoped.java @@ -9,5 +9,5 @@ @Scope @Documented @Retention(RUNTIME) -public @interface DirectoryStreamScoped { +@interface DirectoryStreamScoped { } diff --git a/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java deleted file mode 100644 index 9b167fb2..00000000 --- a/src/test/java/org/cryptomator/cryptofs/dir/ConflictResolverTest.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.cryptomator.cryptofs.dir; - -import com.google.common.base.Strings; -import org.cryptomator.cryptofs.CiphertextFilePath; -import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.cryptomator.cryptofs.common.Constants; -import org.cryptomator.cryptolib.api.Cryptor; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentMatcher; -import org.mockito.Mockito; -import org.mockito.stubbing.Answer; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; - -public class ConflictResolverTest { - - private Path tmpDir; - private LongFileNameProvider longFileNameProvider; - private CryptoPathMapper cryptoPathMapper; - private Cryptor cryptor; - private FileNameCryptor filenameCryptor; - private ConflictResolver conflictResolver; - private String dirId; - - @BeforeEach - public void setup(@TempDir Path tmpDir) { - this.tmpDir = tmpDir; - this.longFileNameProvider = Mockito.mock(LongFileNameProvider.class); - this.cryptoPathMapper = Mockito.mock(CryptoPathMapper.class); - this.cryptor = Mockito.mock(Cryptor.class); - this.filenameCryptor = Mockito.mock(FileNameCryptor.class); - this.conflictResolver = new ConflictResolver(longFileNameProvider, cryptoPathMapper, cryptor); - this.dirId = "foo"; - - Mockito.when(cryptor.fileNameCryptor()).thenReturn(filenameCryptor); - } - - private ArgumentMatcher hasFileName(String name) { - return path -> { - if (path == null) { - return false; - } - Path filename = path.getFileName(); - assert filename != null; - return filename.toString().equals(name); - }; - } - - private Answer fillBufferWithBytes(byte[] bytes) { - return invocation -> { - ByteBuffer buffer = invocation.getArgument(0); - buffer.put(bytes); - return bytes.length; - }; - } - - @ParameterizedTest - @ValueSource(strings = { - ".DS_Store", - "FooBar==.c9r", - "FooBar==.c9s", - }) - public void testPassthroughNonConflictingFiles(String conflictingFileName) throws IOException { - Path conflictingPath = tmpDir.resolve(conflictingFileName); - - Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Assertions.assertSame(conflictingPath, result); - Mockito.verifyNoMoreInteractions(filenameCryptor); - Mockito.verifyNoMoreInteractions(longFileNameProvider); - } - - @ParameterizedTest - @CsvSource({ - "FooBar== (2).c9r,FooBar==.c9r", - "FooBar== (2).c9s,FooBar==.c9s", - }) - public void testResolveTrivially(String conflictingFileName, String expectedCanonicalName) throws IOException { - Path conflictingPath = tmpDir.resolve(conflictingFileName); - Files.createFile(conflictingPath); - - Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Assertions.assertEquals(tmpDir.resolve(expectedCanonicalName), result); - Mockito.verifyNoMoreInteractions(filenameCryptor); - Mockito.verifyNoMoreInteractions(longFileNameProvider); - } - - @ParameterizedTest - @CsvSource({ - "FooBar== (2).c9r,FooBar==.c9r,dir.c9r", - "FooBar== (2).c9s,FooBar==.c9s,symlink.c9r", - }) - public void testResolveTriviallyForIdenticalContent(String conflictingFileName, String expectedCanonicalName, String contentFile) throws IOException { - Path conflictingPath = tmpDir.resolve(conflictingFileName); - Path canonicalPath = tmpDir.resolve(expectedCanonicalName); - Files.createDirectory(conflictingPath); - Files.createDirectory(canonicalPath); - Files.write(conflictingPath.resolve(contentFile), new byte[5]); - Files.write(canonicalPath.resolve(contentFile), new byte[5]); - - Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Assertions.assertEquals(canonicalPath, result); - Mockito.verifyNoMoreInteractions(filenameCryptor); - Mockito.verifyNoMoreInteractions(longFileNameProvider); - } - - @Test - public void testResolveByRenamingRegularFile() throws IOException { - String conflictingName = "FooBar== (2).c9r"; - String canonicalName = "FooBar==.c9r"; - Path conflictingPath = tmpDir.resolve(conflictingName); - Path canonicalPath = tmpDir.resolve(canonicalName); - Files.write(conflictingPath, new byte[3]); - Files.write(canonicalPath, new byte[5]); - - Mockito.when(longFileNameProvider.isDeflated(Mockito.eq(canonicalName))).thenReturn(false); - Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq("FooBar=="), Mockito.any())).thenReturn("cleartext.txt"); - Path resolvedC9rPath = canonicalPath.resolveSibling("BarFoo==.c9r"); - CiphertextFilePath alternativeCiphertextPath = Mockito.mock(CiphertextFilePath.class); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(Mockito.eq(tmpDir), Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"))).thenReturn(alternativeCiphertextPath); - Mockito.when(alternativeCiphertextPath.getRawPath()).thenReturn(resolvedC9rPath); - - Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Mockito.verify(alternativeCiphertextPath).persistLongFileName(); - Assertions.assertEquals(resolvedC9rPath, result); - Assertions.assertFalse(Files.exists(conflictingPath)); - Assertions.assertTrue(Files.exists(result)); - } - - @Test - public void testResolveByRenamingShortenedFile() throws IOException { - String conflictingName = "FooBar== (2).c9s"; - String canonicalName = "FooBar==.c9s"; - String inflatedName = Strings.repeat("a", Constants.MAX_CIPHERTEXT_NAME_LENGTH + 1); - Path conflictingPath = tmpDir.resolve(conflictingName); - Path canonicalPath = tmpDir.resolve(canonicalName); - Files.write(conflictingPath, new byte[3]); - Files.write(canonicalPath, new byte[5]); - - Mockito.when(longFileNameProvider.isDeflated(canonicalName)).thenReturn(true); - Mockito.when(longFileNameProvider.inflate(canonicalPath)).thenReturn(inflatedName); - Mockito.when(filenameCryptor.decryptFilename(Mockito.any(), Mockito.eq(inflatedName), Mockito.any())).thenReturn("cleartext.txt"); - Path resolvedC9sPath = canonicalPath.resolveSibling("BarFoo==.c9s"); - CiphertextFilePath alternativeCiphertextPath = Mockito.mock(CiphertextFilePath.class); - Mockito.when(cryptoPathMapper.getCiphertextFilePath(Mockito.eq(tmpDir), Mockito.any(), Mockito.eq("cleartext.txt (Conflict 1)"))).thenReturn(alternativeCiphertextPath); - Mockito.when(alternativeCiphertextPath.getRawPath()).thenReturn(resolvedC9sPath); - - Path result = conflictResolver.resolveConflictsIfNecessary(conflictingPath, dirId); - - Mockito.verify(alternativeCiphertextPath).persistLongFileName(); - Assertions.assertEquals(resolvedC9sPath, result); - Assertions.assertFalse(Files.exists(conflictingPath)); - Assertions.assertTrue(Files.exists(result)); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java deleted file mode 100644 index cc829535..00000000 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamIntegrationTest.java +++ /dev/null @@ -1,62 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ -package org.cryptomator.cryptofs.dir; - -import org.cryptomator.cryptofs.LongFileNameProvider; -import org.junit.jupiter.api.BeforeEach; - -import java.io.IOException; - -import static org.mockito.Mockito.mock; - -public class CryptoDirectoryStreamIntegrationTest { - - private LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class); - - private CryptoDirectoryStream inTest; - - @BeforeEach - public void setup() throws IOException { - inTest = new CryptoDirectoryStream("foo", null,null, null, null, null); - } - -// @Test -// public void testInflateIfNeededWithShortFilename() { -// String filename = "abc"; -// Path ciphertextPath = Paths.get(filename); -// when(longFileNameProvider.isDeflated(filename)).thenReturn(false); -// -// NodeNames paths = new NodeNames(ciphertextPath); -// -// NodeNames result = inTest.inflateIfNeeded(paths); -// -// MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); -// MatcherAssert.assertThat(result.getInflatedPath(), is(ciphertextPath)); -// MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); -// } -// -// @Test -// public void testInflateIfNeededWithRegularLongFilename() throws IOException { -// String filename = "abc"; -// String inflatedName = Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH + 1); -// Path ciphertextPath = Paths.get(filename); -// Path inflatedPath = Paths.get(inflatedName); -// when(longFileNameProvider.isDeflated(filename)).thenReturn(true); -// when(longFileNameProvider.inflate(ciphertextPath)).thenReturn(inflatedName); -// -// NodeNames paths = new NodeNames(ciphertextPath); -// -// NodeNames result = inTest.inflateIfNeeded(paths); -// -// MatcherAssert.assertThat(result.getCiphertextPath(), is(ciphertextPath)); -// MatcherAssert.assertThat(result.getInflatedPath(), is(inflatedPath)); -// MatcherAssert.assertThat(result.getCleartextPath(), is(nullValue())); -// } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java index a6ef6540..c7c2f0d6 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -41,10 +41,6 @@ public void setup() throws IOException { nodeProcessor = Mockito.mock(NodeProcessor.class); dirStream = Mockito.mock(DirectoryStream.class); } - - private ArgumentMatcher nodeNamed(String name) { - return node -> node.fullCiphertextFileName.equals(name); - } @Test public void testDirListing() throws IOException { From ab6b9ae6990c5e7db65a8aa2203037363499441d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 16:37:51 +0100 Subject: [PATCH 56/62] restored c9s support --- .../cryptofs/dir/C9rProcessor.java | 3 + .../cryptomator/cryptofs/dir/C9sInflator.java | 49 ++++++++++++++ .../cryptofs/dir/C9sProcessor.java | 23 +++++++ .../cryptofs/dir/NodeProcessor.java | 6 +- .../cryptofs/dir/C9SInflatorTest.java | 64 +++++++++++++++++++ 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/C9sInflator.java create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/C9sProcessor.java create mode 100644 src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java index d3d9adba..b72d1962 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.common.Constants; + import javax.inject.Inject; import java.util.stream.Stream; @@ -16,6 +18,7 @@ public C9rProcessor(C9rDecryptor decryptor, C9rConflictResolver conflictResolver } public Stream process(Node node) { + assert node.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX); return decryptor.process(node).flatMap(conflictResolver::process); } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9sInflator.java b/src/main/java/org/cryptomator/cryptofs/dir/C9sInflator.java new file mode 100644 index 00000000..0c699fb3 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9sInflator.java @@ -0,0 +1,49 @@ +package org.cryptomator.cryptofs.dir; + +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.StringUtils; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class C9sInflator { + + private static final Logger LOG = LoggerFactory.getLogger(C9sInflator.class); + + private final LongFileNameProvider longFileNameProvider; + private final Cryptor cryptor; + private final byte[] dirId; + + @Inject + public C9sInflator(LongFileNameProvider longFileNameProvider, Cryptor cryptor, @Named("dirId") String dirId) { + this.longFileNameProvider = longFileNameProvider; + this.cryptor = cryptor; + this.dirId = dirId.getBytes(StandardCharsets.US_ASCII); + } + + public Stream process(Node node) { + try { + String c9rName = longFileNameProvider.inflate(node.ciphertextPath); + node.extractedCiphertext = StringUtils.removeEnd(c9rName, Constants.CRYPTOMATOR_FILE_SUFFIX); + node.cleartextName = cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), node.extractedCiphertext, dirId); + return Stream.of(node); + } catch (AuthenticationFailedException e) { + LOG.warn(node.ciphertextPath + "'s inflated filename could not be decrypted."); + return Stream.empty(); + } catch (IOException e) { + LOG.warn(node.ciphertextPath + " could not be inflated."); + return Stream.empty(); + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9sProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9sProcessor.java new file mode 100644 index 00000000..fef89a28 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9sProcessor.java @@ -0,0 +1,23 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.common.Constants; + +import javax.inject.Inject; +import java.util.stream.Stream; + +@DirectoryStreamScoped +class C9sProcessor { + + private final C9sInflator deflator; + + @Inject + public C9sProcessor(C9sInflator deflator) { + this.deflator = deflator; + } + + public Stream process(Node node) { + assert node.fullCiphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX); + return deflator.process(node); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java index 5afa6bff..bfa677c6 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java @@ -9,11 +9,13 @@ class NodeProcessor { private final C9rProcessor c9rProcessor; + private final C9sProcessor c9sProcessor; private final BrokenDirectoryFilter brokenDirFilter; @Inject - public NodeProcessor(C9rProcessor c9rProcessor, BrokenDirectoryFilter brokenDirFilter){ + public NodeProcessor(C9rProcessor c9rProcessor, C9sProcessor c9sProcessor, BrokenDirectoryFilter brokenDirFilter){ this.c9rProcessor = c9rProcessor; + this.c9sProcessor = c9sProcessor; this.brokenDirFilter = brokenDirFilter; } @@ -21,7 +23,7 @@ public Stream process(Node node) { if (node.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX)) { return c9rProcessor.process(node).flatMap(brokenDirFilter::process); } else if (node.fullCiphertextFileName.endsWith(Constants.DEFLATED_FILE_SUFFIX)) { - return Stream.empty(); + return c9sProcessor.process(node).flatMap(brokenDirFilter::process); } else { return Stream.empty(); } diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java new file mode 100644 index 00000000..1c771b0c --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java @@ -0,0 +1,64 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.LongFileNameProvider; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.stream.Stream; + +class C9SInflatorTest { + + private LongFileNameProvider longFileNameProvider; + private Cryptor cryptor; + private FileNameCryptor fileNameCryptor; + private C9sInflator inflator; + + @BeforeEach + public void setup() { + longFileNameProvider = Mockito.mock(LongFileNameProvider.class); + cryptor = Mockito.mock(Cryptor.class); + fileNameCryptor = Mockito.mock(FileNameCryptor.class); + Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + inflator = new C9sInflator(longFileNameProvider, cryptor, "foo"); + } + + @Test + public void inflateDeflated() throws IOException { + Node deflated = new Node(Paths.get("foo.c9s")); + Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r"); + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("hello world.txt"); + + Stream result = inflator.process(deflated); + Node inflated = result.findAny().get(); + + Assertions.assertEquals("foo", inflated.extractedCiphertext); + Assertions.assertEquals("hello world.txt", inflated.cleartextName); + } + + @Test + public void inflateUninflatableDueToIOException() throws IOException { + Node deflated = new Node(Paths.get("foo.c9s")); + Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenThrow(new IOException("peng!")); + + Stream result = inflator.process(deflated); + Assertions.assertFalse(result.findAny().isPresent()); + } + + @Test + public void inflateUninflatableDueToInvalidCiphertext() throws IOException { + Node deflated = new Node(Paths.get("foo.c9s")); + Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r"); + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenThrow(new AuthenticationFailedException("peng!")); + + Stream result = inflator.process(deflated); + Assertions.assertFalse(result.findAny().isPresent()); + } + +} \ No newline at end of file From 20b101058715cf452ea9a0da72cb9e365b158e3d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 28 Oct 2019 16:43:21 +0100 Subject: [PATCH 57/62] fixed docs from changes in 90cb839...de39a31 this fixes #65 for c9r files --- .../java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java index 60ae067a..73db49ff 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -79,7 +79,7 @@ private Stream resolveConflict(Node conflicting, Path canonicalPath) throw * @param canonicalPath The path to the original (conflict-free) file. * @param conflictingPath The path to the potentially conflicting file. * @param cleartext The cleartext name of the conflicting file. - * @return The NodeNames for the newly created node after renaming the conflicting file. + * @return The newly created Node after renaming the conflicting file. * @throws IOException */ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException { From 4d7528fc04e4cfb92c8b377313aef469bb0da8ee Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 29 Oct 2019 12:16:33 +0100 Subject: [PATCH 58/62] Renamed "BackupUtil" --- .../org/cryptomator/cryptofs/CryptoFileSystemModule.java | 3 ++- .../org/cryptomator/cryptofs/CryptoFileSystemProvider.java | 5 +++-- .../MasterkeyBackupFileHasher.java} | 4 ++-- .../cryptomator/cryptofs/migration/v6/Version6Migrator.java | 4 ++-- .../cryptomator/cryptofs/migration/v7/Version7Migrator.java | 4 ++-- .../cryptofs/migration/v6/Version6MigratorTest.java | 4 ++-- 6 files changed, 13 insertions(+), 11 deletions(-) rename src/main/java/org/cryptomator/cryptofs/{BackupUtil.java => common/MasterkeyBackupFileHasher.java} (91%) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java index c95c03e8..f340d18c 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java @@ -9,6 +9,7 @@ import dagger.Provides; import org.cryptomator.cryptofs.attr.AttributeViewComponent; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; import org.cryptomator.cryptofs.dir.DirectoryStreamComponent; import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent; import org.cryptomator.cryptolib.api.Cryptor; @@ -32,7 +33,7 @@ public Cryptor provideCryptor(CryptorProvider cryptorProvider, @PathToVault Path Path masterKeyPath = pathToVault.resolve(properties.masterkeyFilename()); assert Files.exists(masterKeyPath); // since 1.3.0 a file system can only be created for existing vaults. initialization is done before. byte[] keyFileContents = Files.readAllBytes(masterKeyPath); - Path backupKeyPath = pathToVault.resolve(properties.masterkeyFilename() + BackupUtil.generateFileIdSuffix(keyFileContents) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path backupKeyPath = pathToVault.resolve(properties.masterkeyFilename() + MasterkeyBackupFileHasher.generateFileIdSuffix(keyFileContents) + Constants.MASTERKEY_BACKUP_SUFFIX); Cryptor cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(keyFileContents), properties.passphrase(), properties.pepper(), Constants.VAULT_VERSION); backupMasterkeyFileIfRequired(masterKeyPath, backupKeyPath, readonlyFlag); return cryptor; diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index f0fb1415..7285f69d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -10,6 +10,7 @@ import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel; import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.Cryptor; @@ -227,7 +228,7 @@ public static void changePassphrase(Path pathToVault, String masterkeyFilename, Path masterKeyPath = pathToVault.resolve(masterkeyFilename); byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, pepper, normalizedOldPassphrase, normalizedNewPassphrase); - Path backupKeyPath = pathToVault.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE); } @@ -264,7 +265,7 @@ public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byt Path masterKeyPath = pathToVault.resolve(masterkeyFilename); if (Files.exists(masterKeyPath)) { byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath); - Path backupKeyPath = pathToVault.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX); Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE); } Files.write(masterKeyPath, masterKeyBytes, CREATE_NEW, WRITE); diff --git a/src/main/java/org/cryptomator/cryptofs/BackupUtil.java b/src/main/java/org/cryptomator/cryptofs/common/MasterkeyBackupFileHasher.java similarity index 91% rename from src/main/java/org/cryptomator/cryptofs/BackupUtil.java rename to src/main/java/org/cryptomator/cryptofs/common/MasterkeyBackupFileHasher.java index 9766e1e8..ea9d2f73 100644 --- a/src/main/java/org/cryptomator/cryptofs/BackupUtil.java +++ b/src/main/java/org/cryptomator/cryptofs/common/MasterkeyBackupFileHasher.java @@ -1,4 +1,4 @@ -package org.cryptomator.cryptofs; +package org.cryptomator.cryptofs.common; import com.google.common.io.BaseEncoding; @@ -8,7 +8,7 @@ /** * Utility class for generating a suffix for the backup file to make it unique to its original master key file. */ -public class BackupUtil { +public final class MasterkeyBackupFileHasher { /** * Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format. diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java index 20b43cf4..1505d6aa 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java @@ -15,7 +15,7 @@ import javax.inject.Inject; -import org.cryptomator.cryptofs.BackupUtil; +import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; import org.cryptomator.cryptofs.migration.api.Migrator; @@ -47,7 +47,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 5)) { // create backup, as soon as we know the password was correct: - Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java index 57a0f0ec..01fd9c92 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -5,7 +5,7 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration.v7; -import org.cryptomator.cryptofs.BackupUtil; +import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.DeletingFileVisitor; import org.cryptomator.cryptofs.migration.api.MigrationProgressListener; @@ -51,7 +51,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade); try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) { // create backup, as soon as we know the password was correct: - Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + BackupUtil.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX); Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING); LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName()); diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java index 16e77fd4..74d38267 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java @@ -2,7 +2,7 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.BackupUtil; +import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.mocks.NullSecureRandom; @@ -53,7 +53,7 @@ public void testMigrate() throws IOException { KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile(oldPassword, 5); Assertions.assertEquals(5, beforeMigration.getVersion()); Files.write(masterkeyFile, beforeMigration.serialize()); - Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + BackupUtil.generateFileIdSuffix(beforeMigration.serialize()) + Constants.MASTERKEY_BACKUP_SUFFIX); + Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupFileHasher.generateFileIdSuffix(beforeMigration.serialize()) + Constants.MASTERKEY_BACKUP_SUFFIX); Migrator migrator = new Version6Migrator(cryptorProvider); migrator.migrate(pathToVault, "masterkey.cryptomator", oldPassword); From 1750bafd4d9bde7aece6c778bb7494d3d808d00c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 29 Oct 2019 12:16:43 +0100 Subject: [PATCH 59/62] Added JavaDoc to dir package --- .../org/cryptomator/cryptofs/dir/package-info.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/org/cryptomator/cryptofs/dir/package-info.java diff --git a/src/main/java/org/cryptomator/cryptofs/dir/package-info.java b/src/main/java/org/cryptomator/cryptofs/dir/package-info.java new file mode 100644 index 00000000..e4f64f92 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/package-info.java @@ -0,0 +1,12 @@ +/** + * This package contains classes used during directory listing. + *

+ * When calling {@link java.nio.file.Files#newDirectoryStream(java.nio.file.Path) Files.newDirectoryStream(cleartextPath)}, + * {@link org.cryptomator.cryptofs.dir.DirectoryStreamFactory} will determine the corresponding ciphertextPath + * and open a DirectoryStream on it. + *

+ * Each node will then be passed through a pipes-and-filters system consisting of the vairous classes in this package, resulting in cleartext nodes. + *

+ * As a side effect certain auto-repair steps are applied, if non-standard ciphertext files are encountered and deemed recoverable. + */ +package org.cryptomator.cryptofs.dir; \ No newline at end of file From aed7ef5c3a3ca4485f7d92559eef0fd2ec921049 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 31 Oct 2019 01:15:09 +0100 Subject: [PATCH 60/62] bumped version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fa39428a..650d2782 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 1.9.0-SNAPSHOT + 1.9.0 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs From 93aed7a8cd997d0a35f4bc0d7755e4e87620bad8 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 11 Nov 2019 11:00:45 +0100 Subject: [PATCH 61/62] removed unused code --- .../org/cryptomator/cryptofs/CryptoFileSystemImpl.java | 8 ++------ .../cryptomator/cryptofs/CryptoFileSystemImplTest.java | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index b846b79a..64faf424 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -71,9 +71,7 @@ @CryptoFileSystemScoped class CryptoFileSystemImpl extends CryptoFileSystem { - - private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemImpl.class); - + private final CryptoFileSystemProvider provider; private final CryptoFileSystems cryptoFileSystems; private final Path pathToVault; @@ -81,7 +79,6 @@ class CryptoFileSystemImpl extends CryptoFileSystem { private final CryptoFileStore fileStore; private final CryptoFileSystemStats stats; private final CryptoPathMapper cryptoPathMapper; - private final LongFileNameProvider longFileNameProvider; private final CryptoPathFactory cryptoPathFactory; private final PathMatcherFactory pathMatcherFactory; private final DirectoryStreamFactory directoryStreamFactory; @@ -102,7 +99,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem { @Inject public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, @PathToVault Path pathToVault, Cryptor cryptor, - CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider, CryptoPathFactory cryptoPathFactory, + CryptoFileStore fileStore, CryptoFileSystemStats stats, CryptoPathMapper cryptoPathMapper, CryptoPathFactory cryptoPathFactory, PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider, AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider, OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag, RootDirectoryInitializer rootDirectoryInitializer) { @@ -113,7 +110,6 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems this.fileStore = fileStore; this.stats = stats; this.cryptoPathMapper = cryptoPathMapper; - this.longFileNameProvider = longFileNameProvider; this.cryptoPathFactory = cryptoPathFactory; this.pathMatcherFactory = pathMatcherFactory; this.directoryStreamFactory = directoryStreamFactory; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 608a3eb6..a665641f 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -86,7 +86,6 @@ public class CryptoFileSystemImplTest { private final OpenCryptoFiles openCryptoFiles = mock(OpenCryptoFiles.class); private final Symlinks symlinks = mock(Symlinks.class); private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class); - private final LongFileNameProvider longFileNameProvider = Mockito.mock(LongFileNameProvider.class); private final DirectoryIdProvider dirIdProvider = mock(DirectoryIdProvider.class); private final AttributeProvider fileAttributeProvider = mock(AttributeProvider.class); private final AttributeByNameProvider fileAttributeByNameProvider = mock(AttributeByNameProvider.class); @@ -111,7 +110,7 @@ public void setup() { when(cryptoPathFactory.emptyFor(any())).thenReturn(empty); inTest = new CryptoFileSystemImpl(provider, cryptoFileSystems, pathToVault, cryptor, - fileStore, stats, cryptoPathMapper, longFileNameProvider, cryptoPathFactory, + fileStore, stats, cryptoPathMapper, cryptoPathFactory, pathMatcherFactory, directoryStreamFactory, dirIdProvider, fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider, openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag, rootDirectoryInitializer); From 2c8b99360c915b510009cdaf47ab943c843b9927 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 11 Nov 2019 12:19:00 +0100 Subject: [PATCH 62/62] Partially reverting 97f473b: Only write name.c9s files once, not during every access. --- .../org/cryptomator/cryptofs/LongFileNameProvider.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index af25330d..e96fa89c 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -21,10 +21,12 @@ import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; 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.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; @@ -111,8 +113,11 @@ public void persist() { private void persistInternal() throws IOException { Path longNameFile = c9sPath.resolve(INFLATED_FILE_NAME); Files.createDirectories(c9sPath); - try (WritableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + try (WritableByteChannel ch = Files.newByteChannel(longNameFile, 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(longNameFile), longName.getBytes(UTF_8)); } } }