diff --git a/pom.xml b/pom.xml index 537f3bda..650d2782 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 1.8.10 + 1.9.0 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs @@ -14,7 +14,7 @@ - 1.2.1 + 1.3.0-beta2 2.24 28.1-jre 1.7.28 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..fdd2c2ed --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java @@ -0,0 +1,66 @@ +package org.cryptomator.cryptofs; + +import org.cryptomator.cryptofs.common.Constants; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Optional; + +public class CiphertextFilePath { + + private final Path path; + private final Optional deflatedFileName; + + CiphertextFilePath(Path path, Optional deflatedFileName) { + this.path = Objects.requireNonNull(path); + this.deflatedFileName = Objects.requireNonNull(deflatedFileName); + } + + public Path getRawPath() { + return path; + } + + public boolean isShortened() { + return deflatedFileName.isPresent(); + } + + 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); + } + + public Path getInflatedNamePath() { + return path.resolve(Constants.INFLATED_FILE_NAME); + } + + @Override + public int hashCode() { + 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.deflatedFileName.equals(other.deflatedFileName); + } else { + return false; + } + } + + @Override + public String toString() { + return path.toString(); + } + + public void persistLongFileName() { + deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + } +} diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java deleted file mode 100644 index 1c50bc42..00000000 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextFileType.java +++ /dev/null @@ -1,35 +0,0 @@ -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; - } - - 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()); - } -} diff --git a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java deleted file mode 100644 index 3bc97e2f..00000000 --- a/src/main/java/org/cryptomator/cryptofs/ConflictResolver.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.cryptomator.cryptofs; - -import com.google.common.base.Preconditions; -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.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.SHORT_NAMES_MAX_LENGTH; -import static org.cryptomator.cryptofs.LongFileNameProvider.LONG_NAME_FILE_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 int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes - - private final LongFileNameProvider longFileNameProvider; - private final Cryptor cryptor; - - @Inject - public ConflictResolver(LongFileNameProvider longFileNameProvider, Cryptor cryptor) { - this.longFileNameProvider = longFileNameProvider; - 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(); - String basename = StringUtils.removeEnd(ciphertextFileName, LONG_NAME_FILE_EXT); - Matcher m = CIPHERTEXT_FILENAME_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); - } else { - // full match or no match at all -> nothing to resolve - return ciphertextPath; - } - } - - /** - * 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 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)) { - return canonicalPath; - } else { - return renameConflictingFile(canonicalPath, conflictingPath, ciphertext, dirId, type.getPrefix()); - } - } - - /** - * 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. - * @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 { - 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; - if (alternativeCiphertextFileName.length() > SHORT_NAMES_MAX_LENGTH) { - alternativeCiphertextFileName = longFileNameProvider.deflate(alternativeCiphertextFileName); - } - alternativePath = canonicalPath.resolveSibling(alternativeCiphertextFileName); - } - LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath); - Path resolved = Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE); - longFileNameProvider.getCached(resolved).ifPresent(LongFileNameProvider.DeflatedFileName::persist); - 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); - return conflictingPath; - } - } - - /** - * Tries to resolve a conflicting directory 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 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 { - if (!Files.exists(canonicalPath)) { - Files.move(conflictingPath, canonicalPath, StandardCopyOption.ATOMIC_MOVE); - 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); - 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 - * @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 { - 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); - int read1 = in1.read(buf1); - int read2 = in2.read(buf2); - buf1.flip(); - buf2.flip(); - return read1 == read2 && buf1.compareTo(buf2) == 0; - } - } - -} 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 3ab2b461..00000000 --- a/src/main/java/org/cryptomator/cryptofs/Constants.java +++ /dev/null @@ -1,25 +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 = 6; - 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 = 129; - static final String ROOT_DIR_ID = ""; - - 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/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/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java deleted file mode 100644 index 07725c3e..00000000 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ /dev/null @@ -1,232 +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; - -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.api.FileNameCryptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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; - -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; - -class CryptoDirectoryStream implements DirectoryStream { - - private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class); - - 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 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 { - this.onClose = onClose; - this.finallyUtil = finallyUtil; - this.encryptedNamePattern = encryptedNamePattern; - this.directoryId = ciphertextDir.dirId; - this.ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); - LOG.trace("OPEN {}", directoryId); - this.cleartextDir = cleartextDir; - this.filenameCryptor = filenameCryptor; - this.cryptoPathMapper = cryptoPathMapper; - this.longFileNameProvider = longFileNameProvider; - this.conflictResolver = conflictResolver; - this.filter = filter; - } - - @Override - public Iterator iterator() { - return cleartextDirectoryListing().iterator(); - } - - private Stream cleartextDirectoryListing() { - return directoryListing() // - .map(ProcessedPaths::getCleartextPath) // - .filter(this::isAcceptableByFilter); - } - - public Stream ciphertextDirectoryListing() { - return directoryListing().map(ProcessedPaths::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); - return sanitized; - } - - private ProcessedPaths resolveConflictingFileIfNeeded(ProcessedPaths 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; - } - } - - ProcessedPaths inflateIfNeeded(ProcessedPaths paths) { - String fileName = paths.getCiphertextPath().getFileName().toString(); - 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)); - } - } catch (IOException e) { - LOG.warn(paths.getCiphertextPath() + " could not be inflated."); - return null; - } - } else { - return paths.withInflatedPath(paths.getCiphertextPath()); - } - } - - private ProcessedPaths decrypt(ProcessedPaths paths) { - Optional ciphertextName = encryptedNamePattern.extractEncryptedNameFromStart(paths.getInflatedPath()); - if (ciphertextName.isPresent()) { - String ciphertext = ciphertextName.get(); - try { - String cleartext = filenameCryptor.decryptFilename(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(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())) { - 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); - } catch (IOException e) { - // as defined by DirectoryStream's contract: - // > If an I/O error is encountered when accessing the directory then it - // > causes the {@code Iterator}'s {@code hasNext} or {@code next} methods to - // > throw {@link DirectoryIteratorException} with the {@link IOException} as the - // > cause. - throw new DirectoryIteratorException(e); - } - } - - @Override - @SuppressWarnings("unchecked") - public void close() throws IOException { - finallyUtil.guaranteeInvocationOf( // - () -> ciphertextDirStream.close(), // - () -> onClose.accept(this), // - () -> LOG.trace("CLOSE {}", directoryId)); - } - - 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/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/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index ece13e75..727aae13 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -13,6 +13,12 @@ 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; +import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptolib.api.Cryptor; import org.slf4j.Logger; @@ -56,19 +62,16 @@ 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 { - - private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemImpl.class); - + private final CryptoFileSystemProvider provider; private final CryptoFileSystems cryptoFileSystems; private final Path pathToVault; @@ -76,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; @@ -97,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) { @@ -108,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; @@ -296,16 +297,18 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws throw new NoSuchFileException(cleartextParentDir.toString()); } cryptoPathMapper.assertNonExisting(cleartextDir); - Path ciphertextDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextDir, CiphertextFileType.DIRECTORY); + CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); + Path ciphertextDirFile = ciphertextPath.getDirFilePath(); CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); // atomically check for FileAlreadyExists and create otherwise: + 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))); } // 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); @@ -350,14 +353,19 @@ FileChannel newFileChannel(CryptoPath cleartextPath, Set o } private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute... attrs) throws IOException { - Path ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath, CiphertextFileType.FILE); - 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); + 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 if (options.writable()) { - longFileNameProvider.getCached(ciphertextPath).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + ciphertextPath.persistLongFileName(); } return ch; } @@ -366,26 +374,23 @@ private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOp void delete(CryptoPath cleartextPath) throws IOException { readonlyFlag.assertWritable(); CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath); + CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); switch (ciphertextFileType) { case DIRECTORY: - deleteDirectory(cleartextPath); + deleteDirectory(cleartextPath, ciphertextPath); return; default: - Path ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath, ciphertextFileType); - Files.deleteIfExists(ciphertextFilePath); + Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE); return; } } - private void deleteDirectory(CryptoPath cleartextPath) throws IOException { + private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException { Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path; - Path ciphertextDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY); + Path ciphertextDirFile = ciphertextPath.getDirFilePath(); 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.getRawPath(), DeletingFileVisitor.INSTANCE); cryptoPathMapper.invalidatePathMapping(cleartextPath); dirIdProvider.delete(ciphertextDirFile); } catch (NoSuchFileException e) { @@ -423,13 +428,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, CiphertextFileType.SYMLINK); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.SYMLINK); + 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); - deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); - + Files.copy(ciphertextSourceFile.getRawPath(), ciphertextTargetFile.getRawPath(), resolvedOptions); + ciphertextTargetFile.persistLongFileName(); } else { CryptoPath resolvedSource = symlinks.resolveRecursively(cleartextSource); CryptoPath resolvedTarget = symlinks.resolveRecursively(cleartextTarget); @@ -439,21 +442,22 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget, } private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { - Path ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource, CiphertextFileType.FILE); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE); - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetFile); - Files.copy(ciphertextSourceFile, ciphertextTargetFile, options); - deflatedFileName.ifPresent(LongFileNameProvider.DeflatedFileName::persist); + CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource); + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + if (ciphertextTarget.isShortened()) { + Files.createDirectories(ciphertextTarget.getRawPath()); + } + Files.copy(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); + ciphertextTarget.persistLongFileName(); } private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException { // DIRECTORY (non-recursive as per contract): - Path ciphertextTargetDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.DIRECTORY); - if (Files.notExists(ciphertextTargetDirFile)) { + CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget); + if (Files.notExists(ciphertextTarget.getRawPath())) { // create new: - Optional deflatedFileName = longFileNameProvider.getCached(ciphertextTargetDirFile); 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; @@ -463,7 +467,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; @@ -528,11 +532,15 @@ 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); - 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); + if (ciphertextTarget.isShortened()) { + ciphertextTarget.persistLongFileName(); + } else { + Files.deleteIfExists(ciphertextTarget.getInflatedNamePath()); // no longer needed if not shortened + } twoPhaseMove.commit(); } } @@ -540,11 +548,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, CiphertextFileType.FILE); - Path ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget, CiphertextFileType.FILE); - 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()); + ciphertextTarget.persistLongFileName(); + } + Files.move(ciphertextSource.getFilePath(), ciphertextTarget.getFilePath(), options); + if (ciphertextSource.isShortened()) { + Files.walkFileTree(ciphertextSource.getRawPath(), DeletingFileVisitor.INSTANCE); + } twoPhaseMove.commit(); } } @@ -552,34 +566,39 @@ 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); - 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); - } 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(ciphertextTargetDirFile)) { - Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; - try (DirectoryStream ds = Files.newDirectoryStream(ciphertextTargetDir)) { - if (ds.iterator().hasNext()) { - throw new DirectoryNotEmptyException(cleartextTarget.toString()); - } + 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)) { + 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()); } - Files.delete(ciphertextTargetDir); + } catch (NoSuchFileException e) { + oldCiphertextDirExists = false; + } + // cleanup dir to be replaced: + if (oldCiphertextDirExists) { + Files.walkFileTree(oldCiphertextDir, DeletingFileVisitor.INSTANCE); } - Files.move(ciphertextSourceDirFile, ciphertextTargetDirFile, options); - longFileNameProvider.getCached(ciphertextTargetDirFile).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + Files.walkFileTree(ciphertextTarget.getRawPath(), DeletingFileVisitor.INSTANCE); + } + + // no exceptions until this point, so MOVE: + Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options); + if (ciphertextTarget.isShortened()) { + ciphertextTarget.persistLongFileName(); + } else { + Files.deleteIfExists(ciphertextTarget.getInflatedNamePath()); // no longer needed if not shortened } - dirIdProvider.move(ciphertextSourceDirFile, ciphertextTargetDirFile); - cryptoPathMapper.invalidatePathMapping(cleartextSource); + dirIdProvider.move(ciphertextSource.getDirFilePath(), ciphertextTarget.getDirFilePath()); + cryptoPathMapper.movePathMapping(cleartextSource, cleartextTarget); } CryptoFileStore getFileStore() { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java index 90cd0eac..f340d18c 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java @@ -7,6 +7,11 @@ import dagger.Module; 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; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.KeyFile; @@ -18,7 +23,7 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -@Module +@Module(subcomponents = {AttributeViewComponent.class, OpenCryptoFileComponent.class, DirectoryStreamComponent.class}) class CryptoFileSystemModule { @Provides @@ -28,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 22237704..7285f69d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -8,6 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptofs; +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; @@ -166,8 +169,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); } @@ -227,11 +228,49 @@ 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); } + /** + * 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 + * @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 + MasterkeyBackupFileHasher.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 */ @@ -257,16 +296,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/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java index 02b9b46d..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; @@ -24,7 +26,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 832b8524..df2a81cb 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -13,6 +13,9 @@ 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.cryptofs.common.CiphertextFileType; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptolib.api.Cryptor; import javax.inject.Inject; @@ -20,14 +23,16 @@ 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; 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; +import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; @CryptoFileSystemScoped public class CryptoPathMapper { @@ -65,8 +70,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()); + CiphertextFilePath ciphertextPath = getCiphertextFilePath(cleartextPath); + BasicFileAttributes attr = Files.readAttributes(ciphertextPath.getRawPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attr != null) { + throw new FileAlreadyExistsException(cleartextPath.toString()); + } } catch (NoSuchFileException e) { // good! } @@ -83,52 +91,59 @@ public CiphertextFileType getCiphertextFileType(CryptoPath cleartextPath) throws 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 = getCiphertextFileName(parent.dirId, cleartextName, type); - 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); + 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()) { + if (Files.exists(ciphertextPath.getDirFilePath(), LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.DIRECTORY; + } else if (Files.exists(ciphertextPath.getSymlinkFilePath(), LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.SYMLINK; + } else if (Files.exists(ciphertextPath.getFilePath(), LinkOption.NOFOLLOW_LINKS)) { + return CiphertextFileType.FILE; } } - throw notFound; + throw new NoSuchFileException(cleartextPath.toString(), null, "Could not determine type of file " + ciphertextPath.getRawPath()); } } - public Path getCiphertextFilePath(CryptoPath cleartextPath, CiphertextFileType type) 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); } CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); - String ciphertextName = getCiphertextFileName(parent.dirId, cleartextName, type); - return parent.path.resolve(ciphertextName); + return getCiphertextFilePath(parent.path, parent.dirId, cleartextName); } - - private String getCiphertextFileName(String dirId, String cleartextName, CiphertextFileType fileType) throws IOException { - String ciphertextName = fileType.getPrefix() + ciphertextNames.getUnchecked(new DirIdAndName(dirId, cleartextName)); - if (ciphertextName.length() > Constants.SHORT_NAMES_MAX_LENGTH) { - return longFileNameProvider.deflate(ciphertextName); + + 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.MAX_CIPHERTEXT_NAME_LENGTH) { + LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath); + return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName)); } else { - return ciphertextName; + return new CiphertextFilePath(c9rPath, Optional.empty()); } } private String getCiphertextFileName(DirIdAndName dirIdAndName) { - return cryptor.fileNameCryptor().encryptFilename(dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)); + return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; } 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(); @@ -138,7 +153,7 @@ public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOE } else { try { return ciphertextDirectories.get(cleartextPath, () -> { - Path dirIdFile = getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY); + Path dirIdFile = getCiphertextFilePath(cleartextPath).getDirFilePath(); return resolveDirectory(dirIdFile); }); } catch (ExecutionException e) { diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java deleted file mode 100644 index a9755c00..00000000 --- a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.cryptomator.cryptofs; - -import java.io.IOException; -import java.nio.file.ClosedFileSystemException; -import java.nio.file.DirectoryStream.Filter; -import java.nio.file.Path; -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 { - - 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 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; - this.cryptoPathMapper = cryptoPathMapper; - this.finallyUtil = finallyUtil; - this.encryptedNamePattern = encryptedNamePattern; - } - - 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); - if (closed) { - stream.close(); - throw new ClosedFileSystemException(); - } - return stream; - } - - public void close() throws IOException { - closed = true; - finallyUtil.guaranteeInvocationOf( // - streams.keySet().stream() // - .map(stream -> (RunnableThrowingException) () -> stream.close()) // - .iterator()); - } - -} diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java deleted file mode 100644 index 5b4c7967..00000000 --- a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.cryptomator.cryptofs; - -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 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); - if (matcher.find(0)) { - return Optional.of(matcher.group(2)); - } else { - return Optional.empty(); - } - } - -} 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 a206b629..e96fa89c 100644 --- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java @@ -18,6 +18,8 @@ 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; @@ -25,87 +27,77 @@ 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.METADATA_DIR_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 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"; - private final Path metadataRoot; private final ReadonlyFlag readonlyFlag; - private final LoadingCache longNames; + private final LoadingCache longNames; // Maps from c9s paths to inflated filenames @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(INFLATED_FILE_NAME); + try (SeekableByteChannel ch = Files.newByteChannel(longNameFile, StandardOpenOption.READ)) { + if (ch.size() > MAX_FILENAME_BUFFER_SIZE) { + throw new IOException("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(DEFLATED_FILE_SUFFIX); } - 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 DeflatedFileName deflate(Path c9rPath) { + String longFileName = c9rPath.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 cachedLongName = longNames.getIfPresent(shortName); - if (cachedLongName == null) { - longNames.put(shortName, 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); - } - - public Optional getCached(Path ciphertextFile) { - String shortName = ciphertextFile.getFileName().toString(); - String longName = longNames.getIfPresent(shortName); - if (longName != null) { - return Optional.of(new DeflatedFileName(shortName, longName)); - } else { - return Optional.empty(); - } + String shortName = BASE64.encode(hash) + DEFLATED_FILE_SUFFIX; + Path c9sPath = c9rPath.resolveSibling(shortName); + longNames.put(c9sPath, longFileName); + return new DeflatedFileName(c9sPath, longFileName); } 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 +111,13 @@ 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(INFLATED_FILE_NAME); + Files.createDirectories(c9sPath); + 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(file), longName.getBytes(UTF_8)); + assert Arrays.equals(Files.readAllBytes(longNameFile), longName.getBytes(UTF_8)); } } } 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/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 93b837e6..1084e6c2 100644 --- a/src/main/java/org/cryptomator/cryptofs/Symlinks.java +++ b/src/main/java/org/cryptomator/cryptofs/Symlinks.java @@ -1,5 +1,7 @@ package org.cryptomator.cryptofs; +import org.cryptomator.cryptofs.common.CiphertextFileType; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import javax.inject.Inject; @@ -7,10 +9,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 +44,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); + CiphertextFilePath ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath); EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag); ByteBuffer content = UTF_8.encode(target.toString()); - openCryptoFiles.writeCiphertextFile(ciphertextSymlinkFile, openOptions, content); - longFileNameProvider.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 { - Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.SYMLINK); + Path ciphertextSymlinkFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath).getSymlinkFilePath(); 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 +64,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..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; @@ -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).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, type); + 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 7f9e0c9e..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; @@ -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).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, ciphertextFileType); + Path ciphertextPath = pathMapper.getCiphertextFilePath(cleartextPath).getFilePath(); return readAttributes(ciphertextFileType, ciphertextPath, type); } default: 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/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 84% rename from src/main/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannel.java rename to src/main/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannel.java index 5f3f3d2b..b71c1ec0 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; @@ -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(); @@ -84,9 +68,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 +86,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 +99,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/common/CiphertextFileType.java b/src/main/java/org/cryptomator/cryptofs/common/CiphertextFileType.java new file mode 100644 index 00000000..71fb24b5 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/common/CiphertextFileType.java @@ -0,0 +1,10 @@ +package org.cryptomator.cryptofs.common; + +/** + * Filename prefix as defined issue 38. + */ +public enum CiphertextFileType { + FILE, + DIRECTORY, + SYMLINK; +} 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..69dc3eb7 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * 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 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"; + 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 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/DeletingFileVisitor.java b/src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java similarity index 74% rename from src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java rename to src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java index 495ce3f4..75ddde1a 100644 --- a/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java +++ b/src/main/java/org/cryptomator/cryptofs/common/DeletingFileVisitor.java @@ -6,11 +6,12 @@ * 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; 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,9 +21,14 @@ 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 { +public class DeletingFileVisitor extends SimpleFileVisitor { public static final DeletingFileVisitor INSTANCE = new DeletingFileVisitor(); @@ -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); @@ -48,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/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/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/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/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java new file mode 100644 index 00000000..73db49ff --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java @@ -0,0 +1,161 @@ +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.common.Constants; +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(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 canonicalCiphertextFileName = node.extractedCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX; + if (node.fullCiphertextFileName.equals(canonicalCiphertextFileName)) { + return Stream.of(node); + } else { + try { + Path canonicalPath = node.ciphertextPath.resolveSibling(canonicalCiphertextFileName); + return resolveConflict(node, canonicalPath); + } catch (IOException e) { + LOG.error("Failed to resolve conflict for " + node.ciphertextPath, e); + return Stream.empty(); + } + } + } + + private Stream resolveConflict(Node conflicting, Path canonicalPath) throws IOException { + Path conflictingPath = conflicting.ciphertextPath; + if (resolveConflictTrivially(canonicalPath, conflictingPath)) { + Node resolved = new Node(canonicalPath); + resolved.cleartextName = conflicting.cleartextName; + resolved.extractedCiphertext = conflicting.extractedCiphertext; + return Stream.of(resolved); + } else { + return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, conflicting.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 newly created Node after renaming the conflicting file. + * @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) : ""; + 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); + Node node = new Node(alternativePath); + node.cleartextName = alternativeCleartext; + node.extractedCiphertext = alternativeCiphertext; + return node; + } + + + /** + * 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..e3bf1fda --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java @@ -0,0 +1,91 @@ +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(Node node) { + String basename = StringUtils.removeEnd(node.fullCiphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX); + Matcher matcher = BASE64_PATTERN.matcher(basename); + Optional match = extractCiphertext(node, matcher, 0, basename.length()); + if (match.isPresent()) { + return Stream.of(match.get()); + } else { + return Stream.empty(); + } + } + + private Optional extractCiphertext(Node node, Matcher matcher, int start, int end) { + matcher.region(start, end); + if (matcher.find()) { + final MatchResult match = matcher.toMatchResult(); + final String validBase64 = match.group(); + assert validBase64.length() >= 24; + assert match.end() - match.start() >= 24; + try { + 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); + 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(); + } + + // 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; + } + + // 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 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..b72d1962 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rProcessor.java @@ -0,0 +1,25 @@ +package org.cryptomator.cryptofs.dir; + +import org.cryptomator.cryptofs.common.Constants; + +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(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/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/dir/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java new file mode 100644 index 00000000..da186334 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStream.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import java.io.IOException; +import java.nio.file.DirectoryIteratorException; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@DirectoryStreamScoped +public class CryptoDirectoryStream implements DirectoryStream { + + private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class); + + private final String directoryId; + private final DirectoryStream ciphertextDirStream; + private final Path cleartextDir; + private final DirectoryStream.Filter filter; + private final Consumer onClose; + private final NodeProcessor nodeProcessor; + + @Inject + public CryptoDirectoryStream(@Named("dirId") String dirId, DirectoryStream ciphertextDirStream, @Named("cleartextPath") Path cleartextDir, DirectoryStream.Filter filter, Consumer onClose, NodeProcessor nodeProcessor) { + LOG.trace("OPEN {}", dirId); + this.directoryId = dirId; + this.ciphertextDirStream = ciphertextDirStream; + this.cleartextDir = cleartextDir; + this.filter = filter; + this.onClose = onClose; + this.nodeProcessor = nodeProcessor; + } + + @Override + public Iterator iterator() { + return cleartextDirectoryListing().iterator(); + } + + private Stream cleartextDirectoryListing() { + return directoryListing() + .map(node -> cleartextDir.resolve(node.cleartextName)) + .filter(this::isAcceptableByFilter); + } + + Stream ciphertextDirectoryListing() { + return directoryListing().map(node -> node.ciphertextPath); + } + + private Stream directoryListing() { + return StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(Node::new).flatMap(nodeProcessor::process); + } + + private boolean isAcceptableByFilter(Path path) { + try { + return filter.accept(path); + } catch (IOException e) { + // as defined by DirectoryStream's contract: + // > If an I/O error is encountered when accessing the directory then it + // > causes the {@code Iterator}'s {@code hasNext} or {@code next} methods to + // > throw {@link DirectoryIteratorException} with the {@link IOException} as the + // > cause. + throw new DirectoryIteratorException(e); + } + } + + @Override + public void close() throws IOException { + try { + ciphertextDirStream.close(); + LOG.trace("CLOSE {}", directoryId); + } finally { + onClose.accept(this); + } + } + +} 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..800b321c --- /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 dirId(@Named("dirId") String dirId); + + @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 new file mode 100644 index 00000000..bcacb283 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -0,0 +1,75 @@ +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 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.HashMap; +import java.util.Iterator; +import java.util.Map; + +@CryptoFileSystemScoped +public class DirectoryStreamFactory { + + private final CryptoPathMapper cryptoPathMapper; + private final DirectoryStreamComponent.Builder directoryStreamComponentBuilder; + private final Map streams = new HashMap<>(); + + private volatile boolean closed = false; + + @Inject + public DirectoryStreamFactory(CryptoPathMapper cryptoPathMapper, DirectoryStreamComponent.Builder directoryStreamComponentBuilder) { + this.cryptoPathMapper = cryptoPathMapper; + this.directoryStreamComponentBuilder = directoryStreamComponentBuilder; + } + + public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter filter) throws IOException { + if (closed) { + throw new ClosedFileSystemException(); + } + CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); + DirectoryStream ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); + CryptoDirectoryStream cleartextDirStream = directoryStreamComponentBuilder // + .dirId(ciphertextDir.dirId) // + .ciphertextDirectoryStream(ciphertextDirStream) // + .cleartextPath(cleartextDir) // + .filter(filter) // + .onClose(streams::remove) // + .build() // + .directoryStream(); + streams.put(cleartextDirStream, ciphertextDirStream); + return cleartextDirStream; + } + + public synchronized void close() throws IOException { + closed = true; + 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..35dfba07 --- /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) +@interface DirectoryStreamScoped { +} diff --git a/src/main/java/org/cryptomator/cryptofs/dir/Node.java b/src/main/java/org/cryptomator/cryptofs/dir/Node.java new file mode 100644 index 00000000..aa12df1f --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/Node.java @@ -0,0 +1,18 @@ +package org.cryptomator.cryptofs.dir; + +import java.nio.file.Path; +import java.util.Objects; + +class Node { + + public final Path ciphertextPath; + public final String fullCiphertextFileName; + public String extractedCiphertext; + public String cleartextName; + + public Node(Path ciphertextPath) { + this.ciphertextPath = Objects.requireNonNull(ciphertextPath); + 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 new file mode 100644 index 00000000..bfa677c6 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/dir/NodeProcessor.java @@ -0,0 +1,32 @@ +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; + private final C9sProcessor c9sProcessor; + private final BrokenDirectoryFilter brokenDirFilter; + + @Inject + public NodeProcessor(C9rProcessor c9rProcessor, C9sProcessor c9sProcessor, BrokenDirectoryFilter brokenDirFilter){ + this.c9rProcessor = c9rProcessor; + this.c9sProcessor = c9sProcessor; + this.brokenDirFilter = brokenDirFilter; + } + + 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 c9sProcessor.process(node).flatMap(brokenDirFilter::process); + } else { + return Stream.empty(); + } + } + +} 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 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/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/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..d69c2efc 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,19 +42,12 @@ Migrator provideVersion6Migrator(Version6Migrator 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); - // } + @Provides + @IntoMap + @MigratorKey(Migration.SIX_TO_SEVEN) + Migrator provideVersion7Migrator(Version7Migrator migrator) { + return migrator; + } @Documented @Target(METHOD) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index f42d17dc..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,8 @@ 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; 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..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,8 +15,9 @@ import javax.inject.Inject; -import org.cryptomator.cryptofs.BackupUtil; -import org.cryptomator.cryptofs.Constants; +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; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; @@ -38,16 +39,20 @@ 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); 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()); + + 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/FilePathMigration.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java new file mode 100644 index 00000000..4a08097d --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/FilePathMigration.java @@ -0,0 +1,244 @@ +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.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * 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 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 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 + 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 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 oldCanonicalName The inflated old filename without any conflicting pre- or suffixes but including the file type prefix + */ + FilePathMigration(Path oldPath, String oldCanonicalName) { + assert OLD_CANONICAL_FILENAME_PATTERN.matcher(oldCanonicalName).matches(); + this.oldPath = oldPath; + this.oldCanonicalName = oldCanonicalName; + } + + /** + * 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, such as {@link UninflatableFileException}s + */ + public static Optional parse(Path vaultRoot, Path oldPath) throws IOException { + final String oldFileName = oldPath.getFileName().toString(); + final String canonicalOldFileName; + 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); + } 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)); + } + + /** + * 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 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); + } + 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); + } + } + + /** + * 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 + */ + public Path migrate() throws IOException { + 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); + } + } + } + + public Path getOldPath() { + return oldPath; + } + + // visible for testing + String getOldCanonicalName() { + return oldCanonicalName; + } + + /** + * @return {@link #oldCanonicalName} without any preceeding "0" or "1S" in case of dirs or symlinks. + */ + // visible for testing + String getOldCanonicalNameWithoutTypePrefix() { + 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})) + {@value #NEW_REGULAR_SUFFIX} + */ + // visible for testing + String getNewInflatedName() { + 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 BASE64(SHA1(newInflatedName)) + ".c9s" + */ + // visible for testing + String getNewDeflatedName() { + 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; + } + } + + /** + * @return true if {@link #oldCanonicalName} starts with "0" + */ + // visible for testing + boolean isDirectory() { + return oldCanonicalName.startsWith(OLD_DIRECTORY_PREFIX); + } + + /** + * @return true if {@link #oldCanonicalName} starts with "1S" + */ + // visible for testing + boolean isSymlink() { + return oldCanonicalName.startsWith(OLD_SYMLINK_PREFIX); + } + +} 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/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/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..01fd9c92 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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 org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher; +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; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +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.concurrent.atomic.LongAdder; + +public class Version7Migrator implements Migrator { + + private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class); + + private final CryptorProvider cryptorProvider; + + @Inject + public Version7Migrator(CryptorProvider cryptorProvider) { + this.cryptorProvider = cryptorProvider; + } + + @Override + 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); + try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) { + // create backup, as soon as we know the password was correct: + 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()); + + long toBeMigrated = countFileNames(vaultRoot); + if (toBeMigrated > 0) { + migrateFileNames(vaultRoot, progressListener, toBeMigrated); + } + + progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0); + + // remove deprecated /m/ directory + Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE); + + // 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); + } + + 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, MigrationProgressListener progressListener, long totalFiles) throws IOException { + assert totalFiles > 0; + Path dataDir = vaultRoot.resolve("d"); + Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, new MigratingVisitor(vaultRoot, progressListener, totalFiles)); + } + +} 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/ConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java deleted file mode 100644 index 5cbb3ab7..00000000 --- a/src/test/java/org/cryptomator/cryptofs/ConflictResolverTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.cryptomator.cryptofs; - -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.params.ParameterizedTest; -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.Path; -import java.nio.file.spi.FileSystemProvider; - -public class ConflictResolverTest { - - private LongFileNameProvider longFileNameProvider; - private Cryptor cryptor; - private FileNameCryptor filenameCryptor; - private ConflictResolver conflictResolver; - private String dirId; - private Path testFile; - private Path testFileName; - private Path testDir; - private FileSystem testFileSystem; - private FileSystemProvider testFileSystemProvider; - - @BeforeEach - public void setup() { - 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) { - 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; - }; - } - - @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()); - } - - @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); - } - - @Test - public void testPassthroughValidBase32LongFile() throws IOException { - Mockito.when(testFileName.toString()).thenReturn("ABCDEF==.lng"); - Path resolved = conflictResolver.resolveConflictsIfNecessary(testFile, dirId); - 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()); - } - - @ParameterizedTest - @ValueSource(strings = {"ABCDEF== (1).lng", "conflict_ABCDEF==.lng"}) - public void testRenameLongFile(String conflictingFileName) throws IOException { - String longCiphertextName = "ABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGHABCDEFGH2345===="; - 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()); - } - - @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()); - } - - @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()); - } - - @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()); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java deleted file mode 100644 index d45adcc7..00000000 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamIntegrationTest.java +++ /dev/null @@ -1,107 +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; - -import com.google.common.jimfs.Jimfs; -import org.cryptomator.cryptofs.CryptoDirectoryStream.ProcessedPaths; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -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 static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_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 { - - 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, null, longFileNameProvider, null, null, null, null, null); - } - - @Test - public void testInflateIfNeededWithShortFilename() throws IOException { - String filename = "abc"; - Path ciphertextPath = fileSystem.getPath(filename); - Files.createFile(ciphertextPath); - when(longFileNameProvider.isDeflated(filename)).thenReturn(false); - - ProcessedPaths paths = new ProcessedPaths(ciphertextPath); - - ProcessedPaths 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); - 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(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)); - } - - @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)); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java deleted file mode 100644 index f187d166..00000000 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java +++ /dev/null @@ -1,153 +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; - -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; -import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.Cryptors; -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.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 static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -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 FileNameCryptor filenameCryptor; - private Path ciphertextDirPath; - private DirectoryStream dirStream; - private CryptoPathMapper cryptoPathMapper; - private LongFileNameProvider longFileNameProvider; - private ConflictResolver conflictResolver; - private FinallyUtil finallyUtil; - private EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern(); - - @BeforeEach - @SuppressWarnings("unchecked") - public void setup() throws IOException { - filenameCryptor = CRYPTOR_PROVIDER.createNew().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); - finallyUtil = mock(FinallyUtil.class); - Mockito.when(longFileNameProvider.inflate(Mockito.anyString())).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); - 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()); - - 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 - 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("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, - DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { - 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.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("three"), iter.next()); - Assertions.assertTrue(iter.hasNext()); - Assertions.assertEquals(cleartextPath.resolve("four"), iter.next()); - Assertions.assertFalse(iter.hasNext()); - Mockito.verify(dirStream, Mockito.never()).close(); - } - Mockito.verify(dirStream).close(); - } - - @Test - public void testDirListingForEmptyDir() throws IOException { - Path cleartextPath = Paths.get("/foo/bar"); - - Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); - - try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new CiphertextDirectory("foo", ciphertextDirPath), cleartextPath, filenameCryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL, - DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) { - Iterator iter = stream.iterator(); - Assertions.assertFalse(iter.hasNext()); - Assertions.assertThrows(NoSuchElementException.class, () -> { - iter.next(); - }); - } - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 1dd145cd..f75e436d 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -5,6 +5,11 @@ 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; +import org.cryptomator.cryptofs.dir.DirectoryStreamFactory; import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.cryptomator.cryptofs.fh.OpenCryptoFiles.TwoPhaseMove; @@ -14,11 +19,11 @@ import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; 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.mockito.Mockito; -import javax.inject.Named; import java.io.IOException; import java.lang.reflect.Field; import java.nio.ByteBuffer; @@ -51,10 +56,10 @@ 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; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -83,7 +88,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); @@ -108,7 +112,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); @@ -336,101 +340,118 @@ public void testNewWatchServiceThrowsUnsupportedOperationException() throws IOEx inTest.newWatchService(); }); } - + @Nested public class NewFileChannel { - + private final CryptoPath cleartextPath = mock(CryptoPath.class, "cleartext"); - private final Path ciphertextPath = mock(Path.class, "ciphertext"); + private final CryptoPath ciphertextFilePath = mock(CryptoPath.class, "ciphertext"); + private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); private final OpenCryptoFile openCryptoFile = mock(OpenCryptoFile.class); private final FileChannel fileChannel = mock(FileChannel.class); @BeforeEach public void setup() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); - when(cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.FILE)).thenReturn(ciphertextPath); - when(openCryptoFiles.getOrCreate(ciphertextPath)).thenReturn(openCryptoFile); + when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath); + when(openCryptoFiles.getOrCreate(ciphertextFilePath)).thenReturn(openCryptoFile); when(openCryptoFile.newFileChannel(any())).thenReturn(fileChannel); } - + @Test - @Named("newFileChannel read-only") + @DisplayName("newFileChannel read-only") public void testNewFileChannelReadOnly() throws IOException { FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ)); - + Assertions.assertSame(fileChannel, ch); verify(readonlyFlag, Mockito.never()).assertWritable(); } @Test - @Named("newFileChannel read-only with long filename") + @DisplayName("newFileChannel read-only with long filename") public void testNewFileChannelReadOnlyShortened() throws IOException { - LongFileNameProvider.DeflatedFileName deflatedFileName = Mockito.mock(LongFileNameProvider.DeflatedFileName.class); - when(longFileNameProvider.getCached(ciphertextPath)).thenReturn(Optional.of(deflatedFileName)); - FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ)); Assertions.assertSame(fileChannel, ch); verify(readonlyFlag, Mockito.never()).assertWritable(); - verify(deflatedFileName, Mockito.never()).persist(); + verify(ciphertextPath, Mockito.never()).persistLongFileName(); } @Test - @Named("newFileChannel read-write with long filename") + @DisplayName("newFileChannel read-write with long filename") public void testNewFileChannelReadWriteShortened() throws IOException { - LongFileNameProvider.DeflatedFileName deflatedFileName = Mockito.mock(LongFileNameProvider.DeflatedFileName.class); - when(longFileNameProvider.getCached(ciphertextPath)).thenReturn(Optional.of(deflatedFileName)); - FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.WRITE)); Assertions.assertSame(fileChannel, ch); verify(readonlyFlag, Mockito.atLeastOnce()).assertWritable(); - verify(deflatedFileName).persist(); + verify(ciphertextPath).persistLongFileName(); } - + } @Nested 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 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); + 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(ciphertextRawPath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextDirPath.getFileSystem()).thenReturn(physicalFs); + when(ciphertextDirFilePath.getFileSystem()).thenReturn(physicalFs); + 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(ciphertextRawPath, 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(ciphertextRawPath)).thenReturn(true); inTest.delete(cleartextPath); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).deleteIfExists(ciphertextFilePath); + verify(physicalFsProv).deleteIfExists(ciphertextRawPath); } @Test public void testDeleteExistingDirectory() throws IOException { when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.DIRECTORY); - when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false); + when(physicalFsProv.deleteIfExists(ciphertextRawPath)).thenReturn(false); + when(ciphertextPathAttr.isDirectory()).thenReturn(true); + when(physicalFsProv.newDirectoryStream(Mockito.eq(ciphertextRawPath), 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(ciphertextRawPath); verify(dirIdProvider).delete(ciphertextDirFilePath); verify(cryptoPathMapper).invalidatePathMapping(cleartextPath); } @@ -447,7 +468,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(ciphertextRawPath)).thenReturn(false); Mockito.doThrow(new DirectoryNotEmptyException("ciphertextDir")).when(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath); Assertions.assertThrows(DirectoryNotEmptyException.class, () -> { @@ -464,38 +485,47 @@ 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 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/"); + 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); private final FileSystemProvider physicalFsProv = mock(FileSystemProvider.class); @BeforeEach public void setup() throws IOException { + 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(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); - 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(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, CiphertextFileType.FILE)).thenReturn(ciphertextSourceFile); - when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget, CiphertextFileType.FILE)).thenReturn(ciphertextDestinationFile); + when(cryptoPathMapper.getCiphertextFilePath(sourceLinkTarget)).thenReturn(ciphertextSource); + when(cryptoPathMapper.getCiphertextFilePath(destinationLinkTarget)).thenReturn(ciphertextDestination); } @Nested @@ -573,9 +603,9 @@ 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); + verify(cryptoPathMapper).movePathMapping(cleartextSource, cleartextDestination); } @Test @@ -583,8 +613,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 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); @@ -592,10 +627,11 @@ public void moveDirectoryReplaceExisting() throws IOException { inTest.move(cleartextSource, cleartextDestination, StandardCopyOption.REPLACE_EXISTING); verify(readonlyFlag).assertWritable(); - verify(physicalFsProv).delete(ciphertextDestinationDir); - verify(physicalFsProv).move(ciphertextSourceDirFile, ciphertextDestinationDirFile, StandardCopyOption.REPLACE_EXISTING); + 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 @@ -727,7 +763,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); @@ -762,7 +798,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); @@ -785,7 +821,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); @@ -805,7 +841,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); @@ -827,7 +863,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); @@ -942,16 +978,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 ciphertextDirFile = mock(Path.class, "ciphertextDirFile"); - Path ciphertextDirPath = mock(Path.class, "ciphertextDir"); + 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(cryptoPathMapper.getCiphertextFilePath(path, CiphertextFileType.DIRECTORY)).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(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); @@ -967,16 +1009,22 @@ 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 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(cryptoPathMapper.getCiphertextFilePath(path, CiphertextFileType.DIRECTORY)).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(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); @@ -1269,9 +1317,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, CiphertextFileType.FILE)).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/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 diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index 0bca259b..4ba4731e 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; @@ -361,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); @@ -370,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 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/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 9edb5283..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; @@ -19,6 +20,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 +79,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(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); + 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"); Path d0001 = Mockito.mock(Path.class); @@ -99,19 +103,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(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); + 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"); 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(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); - Mockito.when(dirIdProvider.load(d00010rab)).thenReturn("2"); + Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); + Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); + 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"); Path d0002 = Mockito.mock(Path.class); @@ -129,43 +137,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(fileNameCryptor.encryptFilename("foo", "".getBytes())).thenReturn("oof"); - Mockito.when(dirIdProvider.load(d00000oof)).thenReturn("1"); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir); + 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"); 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(fileNameCryptor.encryptFilename("bar", "1".getBytes())).thenReturn("rab"); - Mockito.when(dirIdProvider.load(d00010rab)).thenReturn("2"); + Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); + Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir); + 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"); 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(fileNameCryptor.encryptFilename("baz", "2".getBytes())).thenReturn("zab"); + Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab); + 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"), CiphertextFileType.FILE); + Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")).getRawPath(); 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 { @@ -179,16 +189,22 @@ 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"); - 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); + 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); + 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 +217,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 +229,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 +241,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 +256,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/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index dadc6853..d8d47c54 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -8,8 +8,11 @@ *******************************************************************************/ 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; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -25,7 +28,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.MAX_CIPHERTEXT_NAME_LENGTH; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties; import static org.cryptomator.cryptofs.CryptoFileSystemUri.create; @@ -35,13 +38,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()); } @@ -79,26 +80,28 @@ public void testDeleteCiphertextDirectoryContainingDirectories() throws IOExcept } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IOException { Path cleartextDirectory = fileSystem.getPath("/b"); 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); } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() throws IOException { Path cleartextDirectory = fileSystem.getPath("/c"); 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); } @@ -115,15 +118,14 @@ public void testDeleteNonEmptyDir() throws IOException { } @Test + @Disabled // c9s not yet implemented public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException { Path cleartextDirectory = fileSystem.getPath("/e"); Files.createDirectory(cleartextDirectory); // 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/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java deleted file mode 100644 index f744bedb..00000000 --- a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.cryptomator.cryptofs; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -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 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); - - 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); - - Assertions.assertFalse(result.isPresent()); - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java index 344d0f04..853d2440 100644 --- a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java @@ -45,60 +45,58 @@ 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); - String inflated1 = prov1.inflate(deflated); + LongFileNameProvider prov1 = new LongFileNameProvider(readonlyFlag); + 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(Paths.get(deflated)).ifPresent(LongFileNameProvider.DeflatedFileName::persist); + deflated.persist(); Assertions.assertEquals(1, countFiles(tmpPath)); - LongFileNameProvider prov2 = new LongFileNameProvider(tmpPath, readonlyFlag); - String inflated2 = prov2.inflate(deflated); + LongFileNameProvider prov2 = new LongFileNameProvider(readonlyFlag); + String inflated2 = prov2.inflate(deflated.c9sPath); 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); + 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(); }); } diff --git a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java index 89e3d0d8..0fa01436 100644 --- a/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java +++ b/src/test/java/org/cryptomator/cryptofs/SymlinksTest.java @@ -1,6 +1,6 @@ package org.cryptomator.cryptofs; -import org.cryptomator.cryptofs.fh.OpenCryptoFile; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -11,9 +11,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 +25,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 +34,59 @@ 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 ciphertextRawPath = Mockito.mock(Path.class); + Path symlinkFilePath = Mockito.mock(Path.class); + BasicFileAttributes ciphertextPathAttr = Mockito.mock(BasicFileAttributes.class); + BasicFileAttributes symlinkFilePathAttr = Mockito.mock(BasicFileAttributes.class); + 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(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); + Mockito.when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); + Mockito.when(ciphertextPath.getSymlinkFilePath()).thenReturn(symlinkFilePath); + return ciphertextRawPath; } @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(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 +95,34 @@ 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(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 +131,17 @@ 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(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,28 @@ 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(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..f9269de1 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java @@ -8,7 +8,8 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; @@ -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, CiphertextFileType.FILE)).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, CiphertextFileType.FILE)).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, CiphertextFileType.SYMLINK)).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, CiphertextFileType.FILE)).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/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 ab4beb31..82852965 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributeViewTest.java @@ -1,6 +1,7 @@ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; @@ -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,19 +49,22 @@ 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); - 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); + + 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/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 851c8de1..1ad616ad 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -1,6 +1,7 @@ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; @@ -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,17 +46,20 @@ 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); - 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); + + 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 e82fb700..8a6e9e89 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoPosixFileAttributeViewTest.java @@ -1,6 +1,7 @@ package org.cryptomator.cryptofs.attr; -import org.cryptomator.cryptofs.CiphertextFileType; +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.common.CiphertextFileType; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; @@ -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,18 +56,21 @@ 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); - 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); + + when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); inTest = new CryptoPosixFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, fileAttributeProvider, readonlyFlag); } 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; 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/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 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 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 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..1d214212 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java @@ -0,0 +1,120 @@ +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); + 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() { + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("helloWorld.txt"); + Node input = new Node(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r")); + + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); + + Assertions.assertTrue(optionalResult.isPresent()); + Assertions.assertEquals("helloWorld.txt", optionalResult.get().cleartextName); + } + + @DisplayName("process non-canonical filename") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "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); + } + }); + Node input = new Node(Paths.get(filename)); + + Stream resultStream = decryptor.process(input); + Optional optionalResult = resultStream.findAny(); + + Assertions.assertTrue(optionalResult.isPresent()); + Assertions.assertEquals("helloWorld.txt", 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) { + Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new AuthenticationFailedException("Invalid ciphertext.")); + Node input = new Node(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/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java new file mode 100644 index 00000000..c7c2f0d6 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/dir/CryptoDirectoryStreamTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.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.Path; +import java.nio.file.Paths; +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 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 NodeProcessor nodeProcessor; + private DirectoryStream dirStream; + + @BeforeEach + public void setup() throws IOException { + nodeProcessor = Mockito.mock(NodeProcessor.class); + dirStream = Mockito.mock(DirectoryStream.class); + } + + @Test + public void testDirListing() throws IOException { + Path ciphertextPath = Paths.get("/f00/b4r"); + Path cleartextPath = Paths.get("/foo/bar"); + List ciphertextFileNames = new ArrayList<>(); + 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 -> { + 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 -> { + 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 -> { + 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.fullCiphertextFileName.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("cleartextFile1"), iter.next()); + Assertions.assertTrue(iter.hasNext()); + Assertions.assertEquals(cleartextPath.resolve("cleartextFile2"), iter.next()); + Assertions.assertTrue(iter.hasNext()); + Assertions.assertEquals(cleartextPath.resolve("cleartextDirectory1"), iter.next()); + Assertions.assertFalse(iter.hasNext()); + Mockito.verify(dirStream, Mockito.never()).close(); + } + Mockito.verify(dirStream).close(); + } + + @Test + public void testDirListingForEmptyDir() throws IOException { + Path cleartextPath = Paths.get("/foo/bar"); + + Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator()); + + 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, () -> { + iter.next(); + }); + } + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java similarity index 60% rename from src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java rename to src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index 8a057568..565f4947 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -1,10 +1,12 @@ -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.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; @@ -13,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.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); + when(directoryStreamBuilder.build()).thenReturn(directoryStreamComp); + when(directoryStreamComp.directoryStream()).then(invocation -> mock(CryptoDirectoryStream.class)); when(fileSystem.provider()).thenReturn(provider); } @@ -62,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); @@ -75,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); @@ -102,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"); 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/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java index 8552049d..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,8 +2,8 @@ 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.MasterkeyBackupFileHasher; +import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.mocks.NullSecureRandom; import org.cryptomator.cryptolib.Cryptors; @@ -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); 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..e2962915 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java @@ -0,0 +1,328 @@ +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.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; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +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.nio.file.Paths; +import java.util.Optional; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class FilePathMigrationTest { + + private Path oldPath = Mockito.mock(Path.class, "oldPath"); + + @ParameterizedTest(name = "getOldCanonicalNameWithoutTypePrefix() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,ORSXG5A=", + "0ORSXG5A=,ORSXG5A=", + "1SORSXG5A=,ORSXG5A=", + }) + public void testGetOldCanonicalNameWithoutTypePrefix(String oldCanonicalName, String expectedResult) { + FilePathMigration migration = new FilePathMigration(oldPath, 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, String expectedResult) { + FilePathMigration migration = new FilePathMigration(oldPath, oldCanonicalName); + + Assertions.assertEquals(expectedResult, migration.getNewInflatedName()); + } + + @ParameterizedTest(name = "getNewInflatedName() expected to be {1} for {0}") + @CsvSource({ + "ORSXG5A=,dGVzdA==.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSQ====,dGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRlc3QgdGVzdCB0ZXN0IHRl.c9r", + "ORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG5BAORSXG===,30xtS3YjsiMJRwu1oAVc_0S2aAU=.c9s", + }) + public void testGetNewDeflatedName(String oldCanonicalName, String expectedResult) { + FilePathMigration migration = new FilePathMigration(oldPath, 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(oldPath, 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(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()); + 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("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 = { + "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 + }) + 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({ + "00/000000000000000000000000000000/ORSXG5A=,ORSXG5A=", + "00/000000000000000000000000000000/0ORSXG5A=,0ORSXG5A=", + "00/000000000000000000000000000000/1SORSXG5A=,1SORSXG5A=", + "00/000000000000000000000000000000/conflict_1SORSXG5A=,1SORSXG5A=", + "00/000000000000000000000000000000/1SORSXG5A= (conflict),1SORSXG5A=", + }) + public void testParseNonShortenedFile(String oldPath, String expected) throws IOException { + Path path = dataDir.resolve(oldPath); + + Optional migration = FilePathMigration.parse(vaultRoot, path); + + Assertions.assertTrue(migration.isPresent()); + Assertions.assertEquals(expected, migration.get().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=", + "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); + Path lngFilePath = metaDir.resolve(metadataFilePath); + Files.createDirectories(lngFilePath.getParent()); + Files.write(lngFilePath, expected.getBytes(UTF_8)); + + Optional migration = FilePathMigration.parse(vaultRoot, path); + + 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", + "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); + 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", + "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); + 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)); + } + + + } + +} 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..aa3e758d --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java @@ -0,0 +1,117 @@ +package org.cryptomator.cryptofs.migration.v7; + +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.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; + +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()); + } + } + + @AfterEach + public void teardown() throws IOException { + fs.close(); + } + + @Test + public void testKeyfileGetsUpdates() 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()); + } + + @Test + public void testMDirectoryGetsDeleted() throws IOException { + Migrator migrator = new Version7Migrator(cryptorProvider); + migrator.migrate(vaultRoot, "masterkey.cryptomator", "test"); + + 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)); + } + +}