diff --git a/pom.xml b/pom.xml index 2ee04425..18399081 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 2.4.2 + 2.4.3 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs @@ -18,21 +18,21 @@ 17 - 2.0.3 - 3.19.1 - 2.41 + 2.1.0-rc1 + 4.0.0 + 2.44 31.1-jre - 1.7.36 + 2.0.3 - 5.8.2 - 4.4.0 + 5.9.1 + 4.8.0 2.2 - 7.0.3 - 0.8.7 - 1.6.12 + 7.2.1 + 0.8.8 + 1.6.13 @@ -71,6 +71,12 @@ java-jwt ${jwt.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.14.0-rc1 + com.google.dagger dagger @@ -96,7 +102,7 @@ org.mockito - mockito-core + mockito-inline ${mockito.version} test @@ -125,7 +131,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.1 true @@ -140,19 +146,17 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M5 + 3.0.0-M7 ERROR - - false org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.3.0 maven-source-plugin @@ -168,7 +172,7 @@ maven-javadoc-plugin - 3.3.0 + 3.4.1 attach-javadocs diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index bf99e5b4..08d33a11 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -378,7 +378,7 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists } - FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options); // might throw FileAlreadyExists + FileChannel ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists try { if (options.writable()) { ciphertextPath.persistLongFileName(); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java index 18978cd3..fb79a344 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java @@ -44,6 +44,15 @@ public class CryptoFileSystemProperties extends AbstractMap { static final int DEFAULT_MAX_CLEARTEXT_NAME_LENGTH = LongFileNameProvider.MAX_FILENAME_BUFFER_SIZE; + /** + * Shortening threshold for ciphertext filenames. + * + * @since 2.5.0 + */ + public static final String PROPERTY_SHORTENING_THRESHOLD = "shorteningThreshold"; + + static final int DEFAULT_SHORTENING_THRESHOLD = 220; + /** * Key identifying the key loader used during initialization. * @@ -105,6 +114,7 @@ private CryptoFileSystemProperties(Builder builder) { Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), // Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), // Map.entry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, builder.maxCleartextNameLength), // + Map.entry(PROPERTY_SHORTENING_THRESHOLD, builder.shorteningThreshold), // Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) // ); } @@ -139,6 +149,10 @@ int maxCleartextNameLength() { return (int) get(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH); } + int shorteningThreshold() { + return (int) get(PROPERTY_SHORTENING_THRESHOLD); + } + @Override public Set> entrySet() { return entries; @@ -193,6 +207,7 @@ public static class Builder { private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME; private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME; private int maxCleartextNameLength = DEFAULT_MAX_CLEARTEXT_NAME_LENGTH; + private int shorteningThreshold = DEFAULT_SHORTENING_THRESHOLD; private Builder() { } @@ -203,6 +218,7 @@ private Builder(Map properties) { checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename); checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags); checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength); + checkedSet(Integer.class, PROPERTY_SHORTENING_THRESHOLD, properties, this::withShorteningThreshold); checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo); } @@ -231,6 +247,18 @@ public Builder withMaxCleartextNameLength(int maxCleartextNameLength) { return this; } + /** + * Sets the shortening threshold used during vault initialization. + * + * @param shorteningThreshold The maximum ciphertext filename length not to be shortened + * @return this + * @since 2.5.0 + */ + public Builder withShorteningThreshold(int shorteningThreshold) { + this.shorteningThreshold = shorteningThreshold; + return this; + } + /** * Sets the cipher combo used during vault initialization. diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 6a8c10fd..9de7e051 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -142,7 +142,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope throw new NotDirectoryException(pathToVault.toString()); } byte[] rawKey = new byte[0]; - var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(Constants.DEFAULT_SHORTENING_THRESHOLD).build(); + var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(properties.shorteningThreshold()).build(); try (Masterkey key = properties.keyLoader().loadKey(keyId); Cryptor cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key, strongSecureRandom())) { rawKey = key.getEncoded(); diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java index 17b183b6..94d32b68 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -82,11 +82,13 @@ public String inflate(Path c9sPath) throws IOException { 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 c9sPath = c9rPath.resolveSibling(shortName); - longNames.put(c9sPath, longFileName); - return new DeflatedFileName(c9sPath, longFileName, readonlyFlag); + try (var sha1 = MessageDigestSupplier.SHA1.instance()) { + byte[] hash = sha1.get().digest(longFileNameBytes); + String shortName = BASE64.encode(hash) + DEFLATED_FILE_SUFFIX; + Path c9sPath = c9rPath.resolveSibling(shortName); + longNames.put(c9sPath, longFileName); + return new DeflatedFileName(c9sPath, longFileName, readonlyFlag); + } } public static class DeflatedFileName { diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java index f1aae4f2..7bdacd3c 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java @@ -35,8 +35,8 @@ sealed class CryptoBasicFileAttributes implements BasicFileAttributes public CryptoBasicFileAttributes(BasicFileAttributes delegate, CiphertextFileType ciphertextFileType, Path ciphertextPath, Cryptor cryptor, Optional openCryptoFile) { this.ciphertextFileType = ciphertextFileType; this.size = switch (ciphertextFileType) { - case SYMLINK, DIRECTORY -> delegate.size(); - case FILE -> getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor); + case DIRECTORY -> delegate.size(); + case SYMLINK, FILE -> getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor); }; this.lastModifiedTime = openCryptoFile.map(OpenCryptoFile::getLastModifiedTime).orElseGet(delegate::lastModifiedTime); this.lastAccessTime = openCryptoFile.map(openFile -> FileTime.from(Instant.now())).orElseGet(delegate::lastAccessTime); diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java index 7ad182cf..fe05baa5 100644 --- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -26,7 +26,6 @@ private Constants() { public static final String CONTENTS_FILE_NAME = "contents.c9r"; public static final String INFLATED_FILE_NAME = "name.c9s"; - public static final int DEFAULT_SHORTENING_THRESHOLD = 220; 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 int MIN_CIPHER_NAME_LENGTH = 26; //rounded up base64url encoded (16 bytes IV + 0 bytes empty string) + file suffix = 26 ASCII chars diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java index 98b22756..b41e35fe 100644 --- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java +++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.Optional; @@ -65,7 +66,7 @@ public OpenCryptoFile(FileCloseListener listener, ChunkCache chunkCache, Cryptor * @return A new file channel. Ideally used in a try-with-resource statement. If the channel is not properly closed, this OpenCryptoFile will stay open indefinite. * @throws IOException */ - public synchronized FileChannel newFileChannel(EffectiveOpenOptions options) throws IOException { + public synchronized FileChannel newFileChannel(EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { Path path = currentFilePath.get(); if (options.truncateExisting()) { @@ -75,7 +76,7 @@ public synchronized FileChannel newFileChannel(EffectiveOpenOptions options) thr FileChannel ciphertextFileChannel = null; CleartextFileChannel cleartextFileChannel = null; try { - ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile()); + ciphertextFileChannel = path.getFileSystem().provider().newFileChannel(path, options.createOpenOptionsForEncryptedFile(), attrs); final FileHeader header; final boolean isNewHeader; if (ciphertextFileChannel.size() == 0l) { diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java index 4cc53321..b8bd6791 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/DirIdCheck.java @@ -61,18 +61,22 @@ public void check(Path pathToVault, VaultConfig config, Masterkey masterkey, Cry boolean foundDir = dirVisitor.secondLevelDirs.remove(expectedDir); if (foundDir) { iter.remove(); - resultCollector.accept(new HealthyDir(dirId, dirIdFile, expectedDir)); + if (Files.exists(expectedDir.resolve(Constants.DIR_ID_FILE))) { + resultCollector.accept(new HealthyDir(dirId, dirIdFile, expectedDir)); + } else { + resultCollector.accept(new MissingDirIdBackup(dirId, expectedDir)); + } } } // remaining dirIds (i.e. missing dirs): dirVisitor.dirIds.forEach((dirId, dirIdFile) -> { - resultCollector.accept(new MissingDirectory(dirId, dirIdFile)); + resultCollector.accept(new MissingContentDir(dirId, dirIdFile)); }); // remaining folders (i.e. missing dir.c9r files): dirVisitor.secondLevelDirs.forEach(dir -> { - resultCollector.accept(new OrphanDir(dir)); + resultCollector.accept(new OrphanContentDir(dir)); }); } @@ -83,6 +87,8 @@ static class DirVisitor extends SimpleFileVisitor { private final Consumer resultCollector; public final Map dirIds = new HashMap<>(); // contents of all found dir.c9r files public final Set secondLevelDirs = new HashSet<>(); // all d/2/30 dirs + public final Set c9rDirsWithDirId = new HashSet<>(); // all d/2/30/abcd=.c9r dirs containing a dirId file + public DirVisitor(Path dataDirPath, Consumer resultCollector) { this.dataDirPath = dataDirPath; @@ -93,6 +99,7 @@ public DirVisitor(Path dataDirPath, Consumer resultCollector) @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (Constants.DIR_FILE_NAME.equals(file.getFileName().toString())) { + c9rDirsWithDirId.add(file.getParent()); return visitDirFile(file, attrs); } return FileVisitResult.CONTINUE; @@ -100,12 +107,20 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO private FileVisitResult visitDirFile(Path file, BasicFileAttributes attrs) throws IOException { assert Constants.DIR_FILE_NAME.equals(file.getFileName().toString()); + var parentDirName = file.getParent().getFileName().toString(); + + if (!(parentDirName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX) || parentDirName.endsWith(Constants.DEFLATED_FILE_SUFFIX))) { + LOG.warn("Encountered loose dir.c9r file."); + resultCollector.accept(new LooseDirIdFile(file)); + return FileVisitResult.CONTINUE; + } + if (attrs.size() > Constants.MAX_DIR_FILE_LENGTH) { LOG.warn("Encountered dir.c9r file of size {}", attrs.size()); - resultCollector.accept(new ObeseDirFile(file, attrs.size())); + resultCollector.accept(new ObeseDirIdFile(file, attrs.size())); } else if (attrs.size() == 0) { LOG.warn("Empty dir.c9r file at {}.", file); - resultCollector.accept(new EmptyDirFile(file)); + resultCollector.accept(new EmptyDirIdFile(file)); } else { byte[] bytes = Files.readAllBytes(file); String dirId = new String(bytes, StandardCharsets.UTF_8); @@ -115,6 +130,7 @@ private FileVisitResult visitDirFile(Path file, BasicFileAttributes attrs) throw resultCollector.accept(new DirIdCollision(dirId, file, otherFile)); } else { dirIds.put(dirId, file); + c9rDirsWithDirId.add(file); } } return FileVisitResult.SKIP_SIBLINGS; @@ -128,6 +144,16 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { } return FileVisitResult.CONTINUE; } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) { + var dirName = dir.getFileName().toString(); + if (dirName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX) && !c9rDirsWithDirId.contains(dir)) { + LOG.warn("Missing dirId file for c9r directory {}.", dir); + resultCollector.accept(new MissingDirIdFile(dir)); + } + return FileVisitResult.CONTINUE; + } } } diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirFile.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirIdFile.java similarity index 84% rename from src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirFile.java rename to src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirIdFile.java index 551ed5f2..37401645 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirFile.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/EmptyDirIdFile.java @@ -16,11 +16,11 @@ * * @see org.cryptomator.cryptofs.common.Constants#ROOT_DIR_ID */ -public class EmptyDirFile implements DiagnosticResult { +public class EmptyDirIdFile implements DiagnosticResult { - final Path dirFile; + final Path dirIdFile; - public EmptyDirFile(Path dirFile) {this.dirFile = dirFile;} + public EmptyDirIdFile(Path dirIdFile) {this.dirIdFile = dirIdFile;} @Override public Severity getSeverity() { @@ -29,7 +29,7 @@ public Severity getSeverity() { @Override public String toString() { - return String.format("File %s is empty, expected content", dirFile); + return String.format("File %s is empty, expected content", dirIdFile); } /* @@ -44,6 +44,6 @@ public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Crypt @Override public Map details() { - return Map.of(DIR_ID_FILE, dirFile.toString()); + return Map.of(DIR_ID_FILE, dirIdFile.toString()); } } diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/LooseDirIdFile.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/LooseDirIdFile.java new file mode 100644 index 00000000..eda23267 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/LooseDirIdFile.java @@ -0,0 +1,42 @@ +package org.cryptomator.cryptofs.health.dirid; + +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.cryptomator.cryptofs.health.api.CommonDetailKeys.DIR_ID_FILE; + +public class LooseDirIdFile implements DiagnosticResult { + + final Path dirIdFile; + + LooseDirIdFile(Path dirIdFile) { + this.dirIdFile = dirIdFile; + } + + @Override + public Severity getSeverity() { + return Severity.INFO; + } + + @Override + public String toString() { + return String.format("A dir.c9r without proper parent found: (%s). .", dirIdFile); + } + + @Override + public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor) throws IOException { + Files.deleteIfExists(dirIdFile); + } + + @Override + public Map details() { + return Map.of(DIR_ID_FILE, dirIdFile.toString()); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java new file mode 100644 index 00000000..db519612 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java @@ -0,0 +1,60 @@ +package org.cryptomator.cryptofs.health.dirid; + +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.DirectoryIdBackup; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.common.Constants; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.cryptomator.cryptofs.health.api.CommonDetailKeys.DIR_ID; +import static org.cryptomator.cryptofs.health.api.CommonDetailKeys.DIR_ID_FILE; + +/** + * Valid dir.c9r file, nonexisting content dir + */ +public class MissingContentDir implements DiagnosticResult { + + final String dirId; + final Path dirIdFile; + + MissingContentDir(String dirId, Path dirIdFile) { + this.dirId = dirId; + this.dirIdFile = dirIdFile; + } + + @Override + public Severity getSeverity() { + return Severity.WARN; + } + + @Override + public String toString() { + return String.format("dir.c9r file (%s) points to non-existing directory.", dirIdFile); + } + + @Override + public Map details() { + return Map.of(DIR_ID, dirId, // + DIR_ID_FILE, dirIdFile.toString()); + } + + @Override + public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor) throws IOException { + var dirIdHash = cryptor.fileNameCryptor().hashDirectoryId(dirId); + Path dirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirIdHash.substring(0, 2)).resolve(dirIdHash.substring(2, 30)); + Files.createDirectories(dirPath); + createDirIdBackupFile(cryptor, new CryptoPathMapper.CiphertextDirectory(dirId, dirPath)); + } + + //visible for testing + void createDirIdBackupFile(Cryptor cryptor, CryptoPathMapper.CiphertextDirectory cipherDirObj) throws IOException { + new DirectoryIdBackup(cryptor).execute(cipherDirObj); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java new file mode 100644 index 00000000..e3ee0c0b --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java @@ -0,0 +1,33 @@ +package org.cryptomator.cryptofs.health.dirid; + +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.DirectoryIdBackup; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * The dir id backup file {@value org.cryptomator.cryptofs.common.Constants#DIR_ID_FILE} is missing. + */ +public record MissingDirIdBackup(String dirId, Path cipherDir) implements DiagnosticResult { + + @Override + public Severity getSeverity() { + return Severity.WARN; + } + + @Override + public String toString() { + return String.format("Directory ID backup for directory %s is missing.", cipherDir); + } + + @Override + public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor) throws IOException { + DirectoryIdBackup dirIdBackup = new DirectoryIdBackup(cryptor); + dirIdBackup.execute(new CryptoPathMapper.CiphertextDirectory(dirId, cipherDir)); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdFile.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdFile.java new file mode 100644 index 00000000..766b5e89 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdFile.java @@ -0,0 +1,45 @@ +package org.cryptomator.cryptofs.health.dirid; + +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.CommonDetailKeys; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.Masterkey; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +/** + * A c9r directory without a dirId file. + */ +public class MissingDirIdFile implements DiagnosticResult { + + final Path c9rDirectory; + + public MissingDirIdFile(Path c9rDirectory) { + this.c9rDirectory = c9rDirectory; + } + + @Override + public Severity getSeverity() { + return Severity.WARN; + } + + @Override + public String toString() { + return String.format("Directory to contain dir.c9r exists (%s), but dir.c9r file is missing.", c9rDirectory); + } + + @Override + public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Cryptor cryptor) throws IOException { + Files.deleteIfExists(c9rDirectory); + } + + @Override + public Map details() { + return Map.of(CommonDetailKeys.ENCRYPTED_PATH, c9rDirectory.toString()); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirectory.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirectory.java deleted file mode 100644 index 548275ab..00000000 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirectory.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.cryptomator.cryptofs.health.dirid; - -import org.cryptomator.cryptofs.health.api.DiagnosticResult; - -import java.nio.file.Path; -import java.util.Map; - -import static org.cryptomator.cryptofs.health.api.CommonDetailKeys.DIR_ID; -import static org.cryptomator.cryptofs.health.api.CommonDetailKeys.DIR_ID_FILE; - -/** - * Valid dir.c9r file, nonexisting dir - */ -public class MissingDirectory implements DiagnosticResult { - - //TODO: maybe add not-existing dir path - final String dirId; - final Path file; - - MissingDirectory(String dirId, Path file) { - this.dirId = dirId; - this.file = file; - } - - @Override - public Severity getSeverity() { - return Severity.CRITICAL; - } - - @Override - public String toString() { - return String.format("dir.c9r file (%s) points to non-existing directory.", file); - } - - @Override - public Map details() { - return Map.of(DIR_ID, dirId, // - DIR_ID_FILE, file.toString()); - } - // fix: create dir? -} diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirIdFile.java similarity index 72% rename from src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java rename to src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirIdFile.java index 45627d7a..c45a9ce0 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirFile.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/ObeseDirIdFile.java @@ -11,13 +11,13 @@ /** * The dir.c9r file's size is too large. */ -public class ObeseDirFile implements DiagnosticResult { +public class ObeseDirIdFile implements DiagnosticResult { - final Path dirFile; + final Path dirIdFile; final long size; - ObeseDirFile(Path dirFile, long size) { - this.dirFile = dirFile; + ObeseDirIdFile(Path dirIdFile, long size) { + this.dirIdFile = dirIdFile; this.size = size; } @@ -28,12 +28,12 @@ public Severity getSeverity() { @Override public String toString() { - return String.format("Unexpected file size of %s: %d should be ≤ %d", dirFile, size, Constants.MAX_DIR_FILE_LENGTH); + return String.format("Unexpected file size of %s: %d should be ≤ %d", dirIdFile, size, Constants.MAX_DIR_FILE_LENGTH); } @Override public Map details() { - return Map.of(DIR_ID_FILE, dirFile.toString(), // + return Map.of(DIR_ID_FILE, dirIdFile.toString(), // "Size", Long.toString(size)); } // potential fix: assign new dir id, move target dir diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java similarity index 96% rename from src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanDir.java rename to src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java index 3737d5b9..b0c226ca 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanDir.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java @@ -37,19 +37,19 @@ /** * An orphan directory is a detached node, not referenced by any dir.c9r file. */ -public class OrphanDir implements DiagnosticResult { +public class OrphanContentDir implements DiagnosticResult { - private static final Logger LOG = LoggerFactory.getLogger(OrphanDir.class); + private static final Logger LOG = LoggerFactory.getLogger(OrphanContentDir.class); private static final String FILE_PREFIX = "file"; private static final String DIR_PREFIX = "directory"; private static final String SYMLINK_PREFIX = "symlink"; private static final String LONG_NAME_SUFFIX_BASE = "_withVeryLongName"; - final Path dir; + final Path contentDir; - OrphanDir(Path dir) { - this.dir = dir; + OrphanContentDir(Path contentDir) { + this.contentDir = contentDir; } @Override @@ -59,12 +59,12 @@ public Severity getSeverity() { @Override public String toString() { - return String.format("Orphan directory: %s", dir); + return String.format("Orphan directory: %s", contentDir); } @Override public Map details() { - return Map.of(ENCRYPTED_PATH, dir.toString()); + return Map.of(ENCRYPTED_PATH, contentDir.toString()); } @Override @@ -72,8 +72,8 @@ public void fix(Path pathToVault, VaultConfig config, Masterkey masterkey, Crypt var sha1 = getSha1MessageDigest(); String runId = Integer.toString((short) UUID.randomUUID().getMostSignificantBits(), 32); Path dataDir = pathToVault.resolve(Constants.DATA_DIR_NAME); - Path orphanedDir = dataDir.resolve(this.dir); - String orphanDirIdHash = dir.getParent().getFileName().toString() + dir.getFileName().toString(); + Path orphanedDir = dataDir.resolve(this.contentDir); + String orphanDirIdHash = contentDir.getParent().getFileName().toString() + contentDir.getFileName().toString(); Path recoveryDir = prepareRecoveryDir(pathToVault, cryptor.fileNameCryptor()); if (recoveryDir.toAbsolutePath().equals(orphanedDir.toAbsolutePath())) { diff --git a/src/main/java/org/cryptomator/cryptofs/health/shortened/ShortenedNamesCheck.java b/src/main/java/org/cryptomator/cryptofs/health/shortened/ShortenedNamesCheck.java index bc252716..af587671 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/shortened/ShortenedNamesCheck.java +++ b/src/main/java/org/cryptomator/cryptofs/health/shortened/ShortenedNamesCheck.java @@ -148,8 +148,10 @@ enum SyntaxResult { //visible for testing String deflate(String longFileName) { byte[] longFileNameBytes = longFileName.getBytes(UTF_8); - byte[] hash = MessageDigestSupplier.SHA1.get().digest(longFileNameBytes); - return BASE64URL.encode(hash) + DEFLATED_FILE_SUFFIX; + try (var sha1 = MessageDigestSupplier.SHA1.instance()) { + byte[] hash = sha1.get().digest(longFileNameBytes); + return BASE64URL.encode(hash) + DEFLATED_FILE_SUFFIX; + } } } 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 158efcf3..59852e3d 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -232,8 +232,10 @@ String getNewDeflatedName() throws InvalidOldFilenameException { 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; + try (var sha1 = MessageDigestSupplier.SHA1.instance()) { + byte[] hash = sha1.get().digest(longFileNameBytes); + return BASE64.encode(hash) + NEW_SHORTENED_SUFFIX; + } } else { return inflatedName; } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index b994a2e4..5b0b1521 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -54,6 +54,7 @@ import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.UserPrincipal; import java.nio.file.spi.FileSystemProvider; import java.util.Arrays; @@ -366,7 +367,7 @@ public void setup() throws IOException { when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath); when(openCryptoFiles.getOrCreate(ciphertextFilePath)).thenReturn(openCryptoFile); when(ciphertextFilePath.getName(3)).thenReturn(mock(CryptoPath.class, "path.c9r")); - when(openCryptoFile.newFileChannel(any())).thenReturn(fileChannel); + when(openCryptoFile.newFileChannel(any(), any())).thenReturn(fileChannel); } @Nested @@ -411,6 +412,18 @@ public void testNewFileChannelCreate2() throws IOException { verify(readonlyFlag, Mockito.never()).assertWritable(); } + @Test + @DisplayName("create new and atomically set file attributes") + public void testNewFileChannelCreate3() throws IOException { + Mockito.doReturn(10).when(fileSystemProperties).maxCleartextNameLength(); + var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-x---")); + + FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), attrs); + + Assertions.assertSame(fileChannel, ch); + verify(openCryptoFile).newFileChannel(Mockito.any(), Mockito.eq(attrs)); + } + } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java index 185dbc08..75c808e8 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java @@ -49,6 +49,7 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() { anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_SHORTENING_THRESHOLD, DEFAULT_SHORTENING_THRESHOLD), // anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @@ -60,18 +61,21 @@ public void testFromMap() { map.put(PROPERTY_KEYLOADER, keyLoader); map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename); map.put(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255); + map.put(PROPERTY_SHORTENING_THRESHOLD, 221); map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)); CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build(); MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename)); MatcherAssert.assertThat(inTest.readonly(), is(true)); MatcherAssert.assertThat(inTest.maxCleartextNameLength(), is(255)); + MatcherAssert.assertThat(inTest.shorteningThreshold(), is(221)); MatcherAssert.assertThat(inTest.entrySet(), containsInAnyOrder( // anEntry(PROPERTY_KEYLOADER, keyLoader), // anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255), // + anEntry(PROPERTY_SHORTENING_THRESHOLD, 221), // anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @@ -93,6 +97,7 @@ public void testWrapMapWithTrueReadonly() { anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_SHORTENING_THRESHOLD, DEFAULT_SHORTENING_THRESHOLD), // anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY)))); } @@ -114,6 +119,7 @@ public void testWrapMapWithFalseReadonly() { anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), // anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_SHORTENING_THRESHOLD, DEFAULT_SHORTENING_THRESHOLD), // anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)))); } @@ -165,6 +171,7 @@ public void testWrapMapWithoutReadonly() { anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), // anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), // anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), // + anEntry(PROPERTY_SHORTENING_THRESHOLD, DEFAULT_SHORTENING_THRESHOLD), // anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), // anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class)) ) diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index 3c04a6fa..a480b8c6 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -139,7 +139,7 @@ public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException // a // .. LongNameaaa... - String name = "LongName" + Strings.repeat("a", Constants.DEFAULT_SHORTENING_THRESHOLD); + String name = "LongName" + Strings.repeat("a", CryptoFileSystemProperties.DEFAULT_SHORTENING_THRESHOLD); createFolder(cleartextDirectory, name); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java index ddbd8675..ef12ab0f 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java @@ -89,9 +89,8 @@ public void testSizeOfDirectory() { @Test public void testSizeOfSymlink() { - Mockito.when(delegateAttr.size()).thenReturn(123l); BasicFileAttributes attr = new CryptoBasicFileAttributes(delegateAttr, SYMLINK, ciphertextFilePath, cryptor, Optional.empty()); - Assertions.assertEquals(123l, attr.size()); + Assertions.assertEquals(1337l, attr.size()); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java index 13873697..343aa2bd 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs.fh; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Feature; import com.google.common.jimfs.Jimfs; import org.cryptomator.cryptofs.EffectiveOpenOptions; import org.cryptomator.cryptofs.ReadonlyFlag; @@ -28,6 +30,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.PosixFilePermissions; import java.time.Instant; import java.util.EnumSet; import java.util.concurrent.atomic.AtomicLong; @@ -55,7 +58,7 @@ public class OpenCryptoFileTest { @BeforeAll public static void setup() { - FS = Jimfs.newFileSystem("OpenCryptoFileTest"); + FS = Jimfs.newFileSystem("OpenCryptoFileTest", Configuration.unix().toBuilder().setAttributeViews("basic", "posix").build()); CURRENT_FILE_PATH = new AtomicReference<>(FS.getPath("currentFile")); } @@ -131,8 +134,9 @@ public void testGetSizeBeforeCreatingFileChannel() { @Order(10) @DisplayName("create first FileChannel") public void createFileChannel() throws IOException { + var attrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-x---")); EffectiveOpenOptions options = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), readonlyFlag); - FileChannel ch = openCryptoFile.newFileChannel(options); + FileChannel ch = openCryptoFile.newFileChannel(options, attrs); Assertions.assertSame(cleartextFileChannel, ch); verify(chunkIO).registerChannel(ciphertextChannel.get(), true); } diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java index a60e65af..ea71bd91 100644 --- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java @@ -67,13 +67,11 @@ public void testGetOrCreate() { public void testWriteCiphertextFile() throws IOException { Path path = Paths.get("/foo"); EffectiveOpenOptions openOptions = Mockito.mock(EffectiveOpenOptions.class); - ByteBuffer contents = Mockito.mock(ByteBuffer.class); + ByteBuffer contents = StandardCharsets.UTF_8.encode("hello world"); inTest.writeCiphertextFile(path, openOptions, contents); - ArgumentCaptor bytesWritten = ArgumentCaptor.forClass(ByteBuffer.class); - Mockito.verify(ciphertextFileChannel).write(bytesWritten.capture()); - Assertions.assertEquals(contents, bytesWritten.getValue()); + Mockito.verify(ciphertextFileChannel).write(contents); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/DirIdCheckTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/DirIdCheckTest.java index 2a73e9f5..9fd437e6 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/DirIdCheckTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/DirIdCheckTest.java @@ -38,6 +38,8 @@ public class DirIdCheckTest { BB/bbbb/bar=.c9r/dir.c9r = ffffffffffff-aaaaaaaaaaaa-tttttttttttt BB/bbbb/baz=.c9r/dir.c9r = [EMPTY] BB/bbbb/foo=.c9r/unrelated/dir.c9r = unrelatedfile + BB/bbbb/missing=.c9r + BB/bbbb/dir.c9r = loose CC/cccc/foo=.c9r = file """; @@ -116,10 +118,21 @@ public void testVisitorDetectsReusedDirId() throws IOException { public void testVisitorDetectsObeseDirId() throws IOException { Files.walkFileTree(dataRoot, Set.of(), 4, visitor); - Predicate expectedObeseFile = obeseDirFile -> "/d/BB/bbbb/bar=.c9r/dir.c9r".equals(obeseDirFile.dirFile.toString()); + Predicate expectedObeseFile = obeseDirFile -> "/d/BB/bbbb/bar=.c9r/dir.c9r".equals(obeseDirFile.dirIdFile.toString()); ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(DiagnosticResult.class); Mockito.verify(resultsCollector, Mockito.atLeastOnce()).accept(resultCaptor.capture()); - MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(ObeseDirFile.class, expectedObeseFile, "Obese dir file: /d/BB/bbbb/bar=.c9r/dir.c9r"))); + MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(ObeseDirIdFile.class, expectedObeseFile, "Obese dir file: /d/BB/bbbb/bar=.c9r/dir.c9r"))); + } + + @Test + @DisplayName("detects loose dirID in /d/BB/bbbb/dir.c9r") + public void testVisitorDetectsLooseDirId() throws IOException { + Files.walkFileTree(dataRoot, Set.of(), 4, visitor); + + Predicate expectedLooseFile = looseDirIdFile -> "/d/BB/bbbb/dir.c9r".equals(looseDirIdFile.dirIdFile.toString()); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(DiagnosticResult.class); + Mockito.verify(resultsCollector, Mockito.atLeastOnce()).accept(resultCaptor.capture()); + MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(LooseDirIdFile.class, expectedLooseFile, "Obese dir file: /d/BB/bbbb/bar=.c9r/dir.c9r"))); } @Test @@ -127,10 +140,21 @@ public void testVisitorDetectsObeseDirId() throws IOException { public void testVisitorDetectsEmptyDirId() throws IOException { Files.walkFileTree(dataRoot, Set.of(), 4, visitor); - Predicate expectedEmptyFile = emptyDirFile -> "/d/BB/bbbb/baz=.c9r/dir.c9r".equals(emptyDirFile.dirFile.toString()); + Predicate expectedEmptyFile = emptyDirFile -> "/d/BB/bbbb/baz=.c9r/dir.c9r".equals(emptyDirFile.dirIdFile.toString()); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(DiagnosticResult.class); + Mockito.verify(resultsCollector, Mockito.atLeastOnce()).accept(resultCaptor.capture()); + MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(EmptyDirIdFile.class, expectedEmptyFile, "Empty dir file: /d/BB/bbbb/baz=.c9r/dir.c9r"))); + } + + @Test + @DisplayName("detects missing dirId in /d/BB/bbbb/missing.c9r") + public void testVisitorDetectsMissingDirId() throws IOException { + Files.walkFileTree(dataRoot, Set.of(), 4, visitor); + + Predicate expectedMissingFile = missingDirIdFile -> "/d/BB/bbbb/missing=.c9r".equals(missingDirIdFile.c9rDirectory.toString()); ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(DiagnosticResult.class); Mockito.verify(resultsCollector, Mockito.atLeastOnce()).accept(resultCaptor.capture()); - MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(EmptyDirFile.class, expectedEmptyFile, "Empty dir file: /d/BB/bbbb/baz=.c9r/dir.c9r"))); + MatcherAssert.assertThat(resultCaptor.getAllValues(), Matchers.hasItem(CustomMatchers.matching(MissingDirIdFile.class, expectedMissingFile, "Missing dirId file for: /d/BB/bbbb/missing=.c9r"))); } } diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java new file mode 100644 index 00000000..8a73f7bd --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java @@ -0,0 +1,71 @@ +package org.cryptomator.cryptofs.health.dirid; + +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.Masterkey; +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.api.io.TempDir; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; + +public class MissingContentDirTest { + + @TempDir + public Path pathToVault; + + private MissingContentDir result; + private String dirId; + private Cryptor cryptor; + private FileNameCryptor fileNameCryptor; + + @BeforeEach + public void init() { + Path p = Mockito.mock(Path.class, "ignored"); + dirId = "1234-456789-1234"; + result = new MissingContentDir(dirId, p); + + cryptor = Mockito.mock(Cryptor.class); + fileNameCryptor = Mockito.mock(FileNameCryptor.class); + Mockito.doReturn(fileNameCryptor).when(cryptor).fileNameCryptor(); + Mockito.doReturn(fileNameCryptor).when(cryptor).fileNameCryptor(); + } + + @DisplayName("After fix the content dir including dirId file exists ") + @Test + public void testFix() throws IOException { + var dirIdHash = "ridiculous-30-char-pseudo-hash"; + Mockito.doReturn(dirIdHash).when(fileNameCryptor).hashDirectoryId(dirId); + var resultSpy = Mockito.spy(result); + Mockito.doNothing().when(resultSpy).createDirIdBackupFile(Mockito.any(), Mockito.any()); + + resultSpy.fix(pathToVault, Mockito.mock(VaultConfig.class), Mockito.mock(Masterkey.class), cryptor); + + var expectedPath = pathToVault.resolve("d/ri/diculous-30-char-pseudo-hash"); + ArgumentMatcher cipherDirMatcher = obj -> obj.dirId.equals(dirId) && obj.path.endsWith(expectedPath); + Mockito.verify(resultSpy, Mockito.times(1)).createDirIdBackupFile(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)); + var attr = Assertions.assertDoesNotThrow(() -> Files.readAttributes(expectedPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)); + Assertions.assertTrue(attr.isDirectory()); + } + + @DisplayName("If dirId file creation fails, fix fails ") + @Test + public void testFixFailsOnFailingDirIdFile() throws IOException { + var dirIdHash = "ridiculous-30-char-pseudo-hash"; + Mockito.doReturn(dirIdHash).when(fileNameCryptor).hashDirectoryId(dirId); + var resultSpy = Mockito.spy(result); + Mockito.doThrow(new IOException("Access denied")).when(resultSpy).createDirIdBackupFile(Mockito.any(), Mockito.any()); + + Assertions.assertThrows(IOException.class, () -> resultSpy.fix(pathToVault, Mockito.mock(VaultConfig.class), Mockito.mock(Masterkey.class), cryptor)); + } +} diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java index b4c736dd..dbe177ac 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java @@ -38,7 +38,7 @@ public class OrphanDirTest { @TempDir public Path pathToVault; - private OrphanDir result; + private OrphanContentDir result; private Path dataDir; private Path cipherRoot; private Path cipherRecovery; @@ -49,7 +49,7 @@ public class OrphanDirTest { @BeforeEach public void init() throws IOException { Path p = Mockito.mock(Path.class, "ignored"); - result = new OrphanDir(p); + result = new OrphanContentDir(p); dataDir = pathToVault.resolve("d"); cipherRoot = dataDir.resolve("00/0000"); @@ -220,7 +220,7 @@ public void testPrepareStepParentOrphanedStepParentDir() throws IOException { @Nested class RetrieveDirIdTests { - private OrphanDir resultSpy; + private OrphanContentDir resultSpy; @BeforeEach public void init() { @@ -418,7 +418,7 @@ public void testAdoptOrphanedShortenedMissingNameC9s() throws IOException { @Test @DisplayName("fix() prepares vault, process every resource in orphanDir and deletes orphanDir (dirId not present)") public void testFixNoDirId() throws IOException { - result = new OrphanDir(dataDir.relativize(cipherOrphan)); + result = new OrphanContentDir(dataDir.relativize(cipherOrphan)); var resultSpy = Mockito.spy(result); Path orphan1 = cipherOrphan.resolve("orphan1.c9r"); @@ -448,7 +448,7 @@ public void testFixNoDirId() throws IOException { @Test @DisplayName("fix() does not choke when filename cannot be restored") public void testFixContinuesOnNotRecoverableFilename() throws IOException { - result = new OrphanDir(dataDir.relativize(cipherOrphan)); + result = new OrphanContentDir(dataDir.relativize(cipherOrphan)); var resultSpy = Mockito.spy(result); Path orphan1 = cipherOrphan.resolve("orphan1.c9r"); @@ -485,7 +485,7 @@ public void testFixContinuesOnNotRecoverableFilename() throws IOException { @Test @DisplayName("fix() prepares vault, process every resource (except dirId file) in orphanDir and deletes orphanDir (dirId present)") public void testFixWithDirId() throws IOException { - result = new OrphanDir(dataDir.relativize(cipherOrphan)); + result = new OrphanContentDir(dataDir.relativize(cipherOrphan)); var resultSpy = Mockito.spy(result); var lostName1 = "Brother.sibling"; @@ -533,7 +533,7 @@ public void testFixRepeated() throws IOException { AtomicReference clearStepparentNameRef = new AtomicReference<>(""); - var interruptedResult = new OrphanDir(dataDir.relativize(cipherOrphan)); + var interruptedResult = new OrphanContentDir(dataDir.relativize(cipherOrphan)); var interruptedSpy = Mockito.spy(interruptedResult); Mockito.doReturn(cipherRecovery).when(interruptedSpy).prepareRecoveryDir(pathToVault, fileNameCryptor); Mockito.doAnswer(invocation -> { @@ -541,7 +541,7 @@ public void testFixRepeated() throws IOException { throw new IOException("Interrupt"); }).when(interruptedSpy).prepareStepParent(Mockito.eq(dataDir), Mockito.eq(cipherRecovery), Mockito.eq(cryptor), Mockito.any()); - var continuedResult = new OrphanDir(dataDir.relativize(cipherOrphan)); + var continuedResult = new OrphanContentDir(dataDir.relativize(cipherOrphan)); var continuedSpy = Mockito.spy(continuedResult); Mockito.doReturn(cipherRecovery).when(continuedSpy).prepareRecoveryDir(pathToVault, fileNameCryptor); Mockito.doThrow(IOException.class).when(continuedSpy).prepareStepParent(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); @@ -557,7 +557,7 @@ public void testFixRepeated() throws IOException { @DisplayName("orphaned recovery dir will only be reintegrated") public void testFixOrphanedRecoveryDir() throws IOException { Path orphanedRecovery = dataDir.resolve("11/1111"); - result = new OrphanDir(dataDir.relativize(orphanedRecovery)); + result = new OrphanContentDir(dataDir.relativize(orphanedRecovery)); var resultSpy = Mockito.spy(result); Path orphan1 = orphanedRecovery.resolve("orphan1.c9r"); diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index ca6ee9ce..00000000 --- a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline \ No newline at end of file