diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index 07727e4c..2e2882c9 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -36,3 +36,4 @@ jobs: MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235" \ No newline at end of file diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 59b312e4..5980df5c 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -23,6 +23,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + MAVEN_GPG_KEY_FINGERPRINT: "58117AFA1F85B3EEC154677D615D449FE6E6A235" - name: Slack Notification uses: rtCamp/action-slack-notify@v2 env: diff --git a/pom.xml b/pom.xml index a0fc375d..79aaa787 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 2.7.0 + 2.7.1 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs @@ -23,19 +23,30 @@ 2.51.1 33.2.1-jre 3.1.8 - 2.0.13 + 2.0.16 - 5.10.3 - 5.12.0 + 5.11.3 + 5.14.2 3.0 1.3.0 - 10.0.3 + 3.13.0 + 3.7.1 + 3.5.1 + 3.4.2 + 3.3.1 + 3.10.1 + 3.2.7 + + 11.0.0 1.3.0 0.8.12 1.7.0 + + + @@ -143,7 +154,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + ${mvn-compiler.version} true @@ -155,10 +166,23 @@ + + org.apache.maven.plugins + maven-dependency-plugin + ${mvn-dependency.version} + + + jar-paths-to-properties + + properties + + + + org.apache.maven.plugins maven-surefire-plugin - 3.3.1 + ${mvn-surefire.version} me.fabriciorby @@ -176,16 +200,17 @@ + @{surefire.jacoco.args} -javaagent:${net.bytebuddy:byte-buddy-agent:jar} org.apache.maven.plugins maven-jar-plugin - 3.4.2 + ${mvn-jar.version} maven-source-plugin - 3.3.1 + ${mvn-source.version} attach-sources @@ -197,7 +222,7 @@ maven-javadoc-plugin - 3.8.0 + ${mvn-javadoc.version} attach-javadocs @@ -281,6 +306,9 @@ prepare-agent + + surefire.jacoco.args + report @@ -300,7 +328,7 @@ maven-gpg-plugin - 3.2.4 + ${mvn-gpg.version} sign-artifacts diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java new file mode 100644 index 00000000..699f03db --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirCache.java @@ -0,0 +1,82 @@ +package org.cryptomator.cryptofs; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +/** + * Caches for the cleartext path of a directory its ciphertext path to the content directory. + */ +public class CiphertextDirCache { + + private static final int MAX_CACHED_PATHS = 5000; + private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20); + + private final AsyncCache ciphertextDirectories = Caffeine.newBuilder() // + .maximumSize(MAX_CACHED_PATHS) // + .expireAfterWrite(MAX_CACHE_AGE) // + .buildAsync(); + + /** + * Removes all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}. + * + * @param basePrefix The prefix key which the keys are checked against + */ + void removeAllKeysWithPrefix(CryptoPath basePrefix) { + ciphertextDirectories.asMap().keySet().removeIf(p -> p.startsWith(basePrefix)); + } + + /** + * Remaps all (key,value) entries, where {@code key.startsWith(oldPrefix) == true}. + * The new key is computed by replacing the oldPrefix with the newPrefix. + * + * @param oldPrefix the prefix key which the keys are checked against + * @param newPrefix the prefix key which replaces {@code oldPrefix} + */ + void recomputeAllKeysWithPrefix(CryptoPath oldPrefix, CryptoPath newPrefix) { + var remappedEntries = new ArrayList(); + ciphertextDirectories.asMap().entrySet().removeIf(e -> { + if (e.getKey().startsWith(oldPrefix)) { + var remappedPath = newPrefix.resolve(oldPrefix.relativize(e.getKey())); + return remappedEntries.add(new CacheEntry(remappedPath, e.getValue())); + } else { + return false; + } + }); + remappedEntries.forEach(e -> ciphertextDirectories.put(e.clearPath(), e.cipherDir())); + } + + + /** + * Gets the cipher directory for the given cleartext path. If a cache miss occurs, the mapping is loaded with the {@code ifAbsent} function. + * @param cleartextPath Cleartext path key + * @param ifAbsent Function to compute the (cleartextPath, cipherDir) mapping on a cache miss. + * @return a {@link CiphertextDirectory}, containing the dirId and the ciphertext content directory path + * @throws IOException if the loading function throws an IOException + */ + CiphertextDirectory get(CryptoPath cleartextPath, CipherDirLoader ifAbsent) throws IOException { + var futureMapping = new CompletableFuture(); + var currentMapping = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, futureMapping); + if (currentMapping != null) { + return currentMapping.join(); + } else { + futureMapping.complete(ifAbsent.load()); + return futureMapping.join(); + } + } + + @FunctionalInterface + interface CipherDirLoader { + + CiphertextDirectory load() throws IOException; + } + + private record CacheEntry(CryptoPath clearPath, CompletableFuture cipherDir) { + + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java new file mode 100644 index 00000000..4dc8f025 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectory.java @@ -0,0 +1,21 @@ +package org.cryptomator.cryptofs; + +import java.nio.file.Path; +import java.util.Objects; + +//own file due to dagger + +/** + * Represents a ciphertext directory without it's mount point in the virtual filesystem. + * + * @param dirId The (ciphertext) dir id (not encrypted, just a uuid) + * @param path The path to content directory (which contains the actual encrypted files and links to subdirectories) + */ +public record CiphertextDirectory(String dirId, Path path) { + + public CiphertextDirectory(String dirId, Path path) { + this.dirId = Objects.requireNonNull(dirId); + this.path = Objects.requireNonNull(path); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java index d3d7af7c..b0c67943 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.attr.AttributeByNameProvider; import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; @@ -142,7 +141,7 @@ public Path getCiphertextPath(Path cleartextPath) throws IOException { var p = CryptoPath.castAndAssertAbsolute(cleartextPath); var nodeType = cryptoPathMapper.getCiphertextFileType(p); if (nodeType == CiphertextFileType.DIRECTORY) { - return cryptoPathMapper.getCiphertextDir(p).path; + return cryptoPathMapper.getCiphertextDir(p).path(); } var cipherFile = cryptoPathMapper.getCiphertextFilePath(p); if (nodeType == CiphertextFileType.SYMLINK) { @@ -316,22 +315,22 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute... attrs) throws if (cleartextParentDir == null) { return; } - Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path; + Path ciphertextParentDir = cryptoPathMapper.getCiphertextDir(cleartextParentDir).path(); if (!Files.exists(ciphertextParentDir)) { throw new NoSuchFileException(cleartextParentDir.toString()); } cryptoPathMapper.assertNonExisting(cleartextDir); CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir); Path ciphertextDirFile = ciphertextPath.getDirFilePath(); - CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); + var 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(UTF_8.encode(ciphertextDir.dirId)); + channel.write(UTF_8.encode(ciphertextDir.dirId())); } // 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); + Files.createDirectories(ciphertextDir.path()); dirIdBackup.execute(ciphertextDir); ciphertextPath.persistLongFileName(); } catch (IOException e) { @@ -432,7 +431,7 @@ private void deleteFileOrSymlink(CiphertextFilePath ciphertextPath) throws IOExc } private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) throws IOException { - Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path; + Path ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextPath).path(); Path ciphertextDirFile = ciphertextPath.getDirFilePath(); try { ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath); @@ -505,7 +504,7 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge ciphertextTarget.persistLongFileName(); } else if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) { // keep existing (if empty): - Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; + Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path(); try (DirectoryStream ds = Files.newDirectoryStream(ciphertextTargetDir)) { if (ds.iterator().hasNext()) { throw new DirectoryNotEmptyException(cleartextTarget.toString()); @@ -515,8 +514,8 @@ private void copyDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge throw new FileAlreadyExistsException(cleartextTarget.toString(), null, "Ciphertext file already exists: " + ciphertextTarget); } if (ArrayUtils.contains(options, StandardCopyOption.COPY_ATTRIBUTES)) { - Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path; - Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; + Path ciphertextSourceDir = cryptoPathMapper.getCiphertextDir(cleartextSource).path(); + Path ciphertextTargetDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path(); copyAttributes(ciphertextSourceDir, ciphertextTargetDir); } } @@ -622,7 +621,7 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks."); } // check if dir is empty: - Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path; + Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path(); boolean targetCiphertextDirExists = true; try (DirectoryStream ds = Files.newDirectoryStream(targetCiphertextDirContentDir, DirectoryStreamFilters.EXCLUDE_DIR_ID_BACKUP)) { if (ds.iterator().hasNext()) { diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 3fffd21c..1360d6d7 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -155,7 +155,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2)); Files.createDirectories(vaultCipherRootPath); // create dirId backup: - DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath)); + DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(Constants.ROOT_DIR_ID, vaultCipherRootPath)); } finally { Arrays.fill(rawKey, (byte) 0x00); } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java index c725611e..01633d38 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import com.github.benmanes.caffeine.cache.AsyncCache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.io.BaseEncoding; @@ -27,10 +26,7 @@ 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.CompletableFuture; import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME; @@ -39,8 +35,6 @@ public class CryptoPathMapper { private static final Logger LOG = LoggerFactory.getLogger(CryptoPathMapper.class); private static final int MAX_CACHED_CIPHERTEXT_NAMES = 5000; - private static final int MAX_CACHED_DIR_PATHS = 5000; - private static final Duration MAX_CACHE_AGE = Duration.ofSeconds(20); private final Cryptor cryptor; private final Path dataRoot; @@ -48,7 +42,7 @@ public class CryptoPathMapper { private final LongFileNameProvider longFileNameProvider; private final VaultConfig vaultConfig; private final LoadingCache ciphertextNames; - private final AsyncCache ciphertextDirectories; + private final CiphertextDirCache ciphertextDirCache; private final CiphertextDirectory rootDirectory; @@ -60,7 +54,7 @@ public class CryptoPathMapper { this.longFileNameProvider = longFileNameProvider; this.vaultConfig = vaultConfig; this.ciphertextNames = Caffeine.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(this::getCiphertextFileName); - this.ciphertextDirectories = Caffeine.newBuilder().maximumSize(MAX_CACHED_DIR_PATHS).expireAfterWrite(MAX_CACHE_AGE).buildAsync(); + this.ciphertextDirCache = new CiphertextDirCache(); this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID); } @@ -69,7 +63,7 @@ public class CryptoPathMapper { * * @param cleartextPath A path * @throws FileAlreadyExistsException If the node exists - * @throws IOException If any I/O error occurs while attempting to resolve the ciphertext path + * @throws IOException If any I/O error occurs while attempting to resolve the ciphertext path */ public void assertNonExisting(CryptoPath cleartextPath) throws FileAlreadyExistsException, IOException { try { @@ -121,7 +115,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws } CiphertextDirectory parent = getCiphertextDir(parentPath); String cleartextName = cleartextPath.getFileName().toString(); - return getCiphertextFilePath(parent.path, parent.dirId, cleartextName); + return getCiphertextFilePath(parent.path(), parent.dirId(), cleartextName); } public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) { @@ -136,36 +130,36 @@ public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String } private String getCiphertextFileName(DirIdAndName dirIdAndName) { - return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.name, dirIdAndName.dirId.getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; + return cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), dirIdAndName.clearNodeName(), dirIdAndName.dirId().getBytes(StandardCharsets.UTF_8)) + Constants.CRYPTOMATOR_FILE_SUFFIX; } + /** + * Removes the given cleartext path and all cached child paths from the dir cache + * @param cleartextPath the root cleartext path, for which all mappings starting with it will be removed + */ public void invalidatePathMapping(CryptoPath cleartextPath) { - ciphertextDirectories.asMap().remove(cleartextPath); + ciphertextDirCache.removeAllKeysWithPrefix(cleartextPath); } + /** + * Moves the given cleartext path and all cached child paths in the dir cache + * @param cleartextSrc the root cleartext path, for which alle mappings starting with it will be moved + * @param cleartextDst the destination cleartext path. The path itself and all childs will be adjusted to start with cleartextDst. + */ public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) { - var cachedValue = ciphertextDirectories.asMap().remove(cleartextSrc); - if (cachedValue != null) { - ciphertextDirectories.put(cleartextDst, cachedValue); - } + ciphertextDirCache.recomputeAllKeysWithPrefix(cleartextSrc, cleartextDst); } public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException { assert cleartextPath.isAbsolute(); - CryptoPath parentPath = cleartextPath.getParent(); - if (parentPath == null) { + if (cleartextPath.getParent() == null) { return rootDirectory; } else { - var lazyEntry = new CompletableFuture(); - var priorEntry = ciphertextDirectories.asMap().putIfAbsent(cleartextPath, lazyEntry); - if (priorEntry != null) { - return priorEntry.join(); - } else { + CiphertextDirCache.CipherDirLoader cipherDirLoaderIfAbsent = () -> { Path dirFile = getCiphertextFilePath(cleartextPath).getDirFilePath(); - CiphertextDirectory cipherDir = resolveDirectory(dirFile); - lazyEntry.complete(cipherDir); - return cipherDir; - } + return resolveDirectory(dirFile); + }; + return ciphertextDirCache.get(cleartextPath, cipherDirLoaderIfAbsent); } } @@ -179,57 +173,4 @@ private CiphertextDirectory resolveDirectory(String dirId) { Path dirPath = dataRoot.resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2)); return new CiphertextDirectory(dirId, dirPath); } - - public static class CiphertextDirectory { - public final String dirId; - public final Path path; - - public CiphertextDirectory(String dirId, Path path) { - this.dirId = Objects.requireNonNull(dirId); - this.path = Objects.requireNonNull(path); - } - - @Override - public int hashCode() { - return Objects.hash(dirId, path); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } else if (obj instanceof CiphertextDirectory other) { - return this.dirId.equals(other.dirId) && this.path.equals(other.path); - } else { - return false; - } - } - } - - private static class DirIdAndName { - public final String dirId; - public final String name; - - public DirIdAndName(String dirId, String name) { - this.dirId = Objects.requireNonNull(dirId); - this.name = Objects.requireNonNull(name); - } - - @Override - public int hashCode() { - return Objects.hash(dirId, name); - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } else if (obj instanceof DirIdAndName other) { - return this.dirId.equals(other.dirId) && this.name.equals(other.name); - } else { - return false; - } - } - } - } diff --git a/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java b/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java new file mode 100644 index 00000000..8367e556 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/DirIdAndName.java @@ -0,0 +1,19 @@ +package org.cryptomator.cryptofs; + +import java.util.Objects; + +//own file due to dagger + +/** + * Helper object to store the dir id of a directory along with its cleartext name (aka, the last element in the cleartext path) + * @param dirId + * @param clearNodeName + */ +record DirIdAndName(String dirId, String clearNodeName) { + + public DirIdAndName(String dirId, String clearNodeName) { + this.dirId = Objects.requireNonNull(dirId); + this.clearNodeName = Objects.requireNonNull(clearNodeName); + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java index b3cdc0db..6dc2f7fe 100644 --- a/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java +++ b/src/main/java/org/cryptomator/cryptofs/DirectoryIdBackup.java @@ -26,17 +26,17 @@ public DirectoryIdBackup(Cryptor cryptor) { } /** - * Performs the backup operation for the given {@link CryptoPathMapper.CiphertextDirectory} object. + * Performs the backup operation for the given {@link CiphertextDirectory} object. *

- * The directory id is written via an encrypting channel to the file {@link CryptoPathMapper.CiphertextDirectory#path}/{@value Constants#DIR_BACKUP_FILE_NAME}. + * The directory id is written via an encrypting channel to the file {@link CiphertextDirectory#path()} /{@value Constants#DIR_BACKUP_FILE_NAME}. * * @param ciphertextDirectory The cipher dir object containing the dir id and the encrypted content root * @throws IOException if an IOException is raised during the write operation */ - public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException { - try (var channel = Files.newByteChannel(ciphertextDirectory.path.resolve(Constants.DIR_BACKUP_FILE_NAME), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); // + public void execute(CiphertextDirectory ciphertextDirectory) throws IOException { + try (var channel = Files.newByteChannel(ciphertextDirectory.path().resolve(Constants.DIR_BACKUP_FILE_NAME), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); // var encryptingChannel = wrapEncryptionAround(channel, cryptor)) { - encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId.getBytes(StandardCharsets.US_ASCII))); + encryptingChannel.write(ByteBuffer.wrap(ciphertextDirectory.dirId().getBytes(StandardCharsets.US_ASCII))); } } @@ -44,10 +44,10 @@ public void execute(CryptoPathMapper.CiphertextDirectory ciphertextDirectory) th * Static method to explicitly back up the directory id for a specified ciphertext directory. * * @param cryptor The cryptor to be used - * @param ciphertextDirectory A {@link org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory} for which the dirId should be back up'd. + * @param ciphertextDirectory A {@link CiphertextDirectory} for which the dirId should be back up'd. * @throws IOException when the dirId file already exists, or it cannot be written to. */ - public static void backupManually(Cryptor cryptor, CryptoPathMapper.CiphertextDirectory ciphertextDirectory) throws IOException { + public static void backupManually(Cryptor cryptor, CiphertextDirectory ciphertextDirectory) throws IOException { new DirectoryIdBackup(cryptor).execute(ciphertextDirectory); } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 82563853..7a91c2a6 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -60,7 +60,7 @@ private Path getCiphertextPath(CryptoPath path) throws IOException { yield getCiphertextPath(resolved); } case DIRECTORY: - yield pathMapper.getCiphertextDir(path).path; + yield pathMapper.getCiphertextDir(path).path(); case FILE: yield 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 b2195161..feff23db 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java @@ -53,7 +53,7 @@ public A readAttributes(CryptoPath cleartextPath private Path getCiphertextPath(CryptoPath path, CiphertextFileType type) throws IOException { return switch (type) { case SYMLINK -> pathMapper.getCiphertextFilePath(path).getSymlinkFilePath(); - case DIRECTORY -> pathMapper.getCiphertextDir(path).path; + case DIRECTORY -> pathMapper.getCiphertextDir(path).path(); case FILE -> pathMapper.getCiphertextFilePath(path).getFilePath(); }; } diff --git a/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java index 2c05b34b..ba1e86ad 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilter.java @@ -28,7 +28,7 @@ public Stream process(Node node) { if (Files.isRegularFile(dirFile)) { final Path dirPath; try { - dirPath = cryptoPathMapper.resolveDirectory(dirFile).path; + dirPath = cryptoPathMapper.resolveDirectory(dirFile).path(); } catch (IOException e) { LOG.warn("Broken directory file: " + dirFile, e); return Stream.empty(); diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java index 651d7dff..77ceb6f0 100644 --- a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactory.java @@ -1,9 +1,9 @@ package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.CryptoFileSystemScoped; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.common.Constants; import javax.inject.Inject; @@ -37,8 +37,8 @@ public synchronized CryptoDirectoryStream newDirectoryStream(CryptoPath cleartex throw new ClosedFileSystemException(); } CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir); - DirectoryStream ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, this::matchesEncryptedContentPattern); - var cleartextDirStream = directoryStreamComponentFactory.create(cleartextDir, ciphertextDir.dirId, ciphertextDirStream, filter, streams::remove).directoryStream(); + DirectoryStream ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path(), this::matchesEncryptedContentPattern); + var cleartextDirStream = directoryStreamComponentFactory.create(cleartextDir, ciphertextDir.dirId(), ciphertextDirStream, filter, streams::remove).directoryStream(); streams.put(cleartextDirStream, ciphertextDirStream); return cleartextDirStream; } diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java index 7e59d982..28cd3614 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingContentDir.java @@ -1,6 +1,6 @@ package org.cryptomator.cryptofs.health.dirid; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants; @@ -51,7 +51,7 @@ void fix(Path pathToVault, Cryptor cryptor) throws IOException { var dirIdHash = cryptor.fileNameCryptor().hashDirectoryId(dirId); Path dirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirIdHash.substring(0, 2)).resolve(dirIdHash.substring(2, 32)); Files.createDirectories(dirPath); - DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(dirId, dirPath)); + DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, dirPath)); } @Override diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java index be480db0..9002ba94 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackup.java @@ -1,9 +1,8 @@ package org.cryptomator.cryptofs.health.dirid; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; -import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.health.api.DiagnosticResult; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.Masterkey; @@ -30,7 +29,7 @@ public String toString() { //visible for testing void fix(Path pathToVault, Cryptor cryptor) throws IOException { Path absCipherDir = pathToVault.resolve(contentDir); - DirectoryIdBackup.backupManually(cryptor, new CryptoPathMapper.CiphertextDirectory(dirId, absCipherDir)); + DirectoryIdBackup.backupManually(cryptor, new CiphertextDirectory(dirId, absCipherDir)); } @Override diff --git a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java index 505c0dc7..350616a9 100644 --- a/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java +++ b/src/main/java/org/cryptomator/cryptofs/health/dirid/OrphanContentDir.java @@ -1,7 +1,7 @@ package org.cryptomator.cryptofs.health.dirid; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.CiphertextFileType; @@ -115,7 +115,7 @@ private void fix(Path pathToVault, VaultConfig config, Cryptor cryptor) throws I Files.deleteIfExists(orphanedDir.resolve(Constants.DIR_BACKUP_FILE_NAME)); try (var nonCryptomatorFiles = Files.newDirectoryStream(orphanedDir)) { for (Path p : nonCryptomatorFiles) { - Files.move(p, stepParentDir.path.resolve(p.getFileName()), LinkOption.NOFOLLOW_LINKS); + Files.move(p, stepParentDir.path().resolve(p.getFileName()), LinkOption.NOFOLLOW_LINKS); } } Files.delete(orphanedDir); @@ -154,7 +154,7 @@ Path prepareRecoveryDir(Path pathToVault, FileNameCryptor cryptor) throws IOExce } // visible for testing - CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryptor cryptor, String clearStepParentDirName) throws IOException { + CiphertextDirectory prepareStepParent(Path dataDir, Path cipherRecoveryDir, Cryptor cryptor, String clearStepParentDirName) throws IOException { //create "stepparent" directory to move orphaned files to String cipherStepParentDirName = encrypt(cryptor.fileNameCryptor(), clearStepParentDirName, Constants.RECOVERY_DIR_ID); Path cipherStepParentDirFile = cipherRecoveryDir.resolve(cipherStepParentDirName + "/" + Constants.DIR_FILE_NAME); @@ -169,7 +169,7 @@ CryptoPathMapper.CiphertextDirectory prepareStepParent(Path dataDir, Path cipher String stepParentDirHash = cryptor.fileNameCryptor().hashDirectoryId(stepParentUUID); Path stepParentDir = dataDir.resolve(stepParentDirHash.substring(0, 2)).resolve(stepParentDirHash.substring(2)).toAbsolutePath(); Files.createDirectories(stepParentDir); - var stepParentCipherDir = new CryptoPathMapper.CiphertextDirectory(stepParentUUID, stepParentDir); + var stepParentCipherDir = new CiphertextDirectory(stepParentUUID, stepParentDir); //only if it does not exist try { DirectoryIdBackup.backupManually(cryptor, stepParentCipherDir); @@ -215,11 +215,11 @@ String decryptFileName(Path orphanedResource, boolean isShortened, String dirId, } // visible for testing - void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isShortened, CryptoPathMapper.CiphertextDirectory stepParentDir, FileNameCryptor cryptor, MessageDigest sha1) throws IOException { - var newCipherName = encrypt(cryptor, newClearName, stepParentDir.dirId); + void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isShortened, CiphertextDirectory stepParentDir, FileNameCryptor cryptor, MessageDigest sha1) throws IOException { + var newCipherName = encrypt(cryptor, newClearName, stepParentDir.dirId()); if (isShortened) { var deflatedName = BaseEncoding.base64Url().encode(sha1.digest(newCipherName.getBytes(StandardCharsets.UTF_8))) + Constants.DEFLATED_FILE_SUFFIX; - Path targetPath = stepParentDir.path.resolve(deflatedName); + Path targetPath = stepParentDir.path().resolve(deflatedName); Files.move(oldCipherPath, targetPath); //adjust name.c9s @@ -227,7 +227,7 @@ void adoptOrphanedResource(Path oldCipherPath, String newClearName, boolean isSh fc.write(ByteBuffer.wrap(newCipherName.getBytes(StandardCharsets.UTF_8))); } } else { - Path targetPath = stepParentDir.path.resolve(newCipherName); + Path targetPath = stepParentDir.path().resolve(newCipherName); Files.move(oldCipherPath, targetPath); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java b/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java new file mode 100644 index 00000000..9af0e613 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/CiphertextDirCacheTest.java @@ -0,0 +1,119 @@ +package org.cryptomator.cryptofs; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Path; + +public class CiphertextDirCacheTest { + + CiphertextDirCache cache; + CryptoPath clearPath; + CiphertextDirCache.CipherDirLoader dirLoader; + + + @BeforeEach + public void beforeEach() throws IOException { + cache = new CiphertextDirCache(); + clearPath = Mockito.mock(CryptoPath.class); + dirLoader = Mockito.mock(CiphertextDirCache.CipherDirLoader.class); + var cipherDir = Mockito.mock(CiphertextDirectory.class); + Mockito.when(dirLoader.load()).thenReturn(cipherDir); + } + + @Test + public void testPuttingNewEntryTriggersLoader() throws IOException { + var cipherDir = Mockito.mock(CiphertextDirectory.class); + Mockito.when(dirLoader.load()).thenReturn(cipherDir); + + var result = cache.get(clearPath, dirLoader); + Assertions.assertEquals(cipherDir, result); + Mockito.verify(dirLoader).load(); + } + + @Test + public void testPuttingKnownEntryDoesNotTriggerLoader() throws IOException { + Mockito.when(dirLoader.load()).thenReturn(Mockito.mock(CiphertextDirectory.class)); + var dirLoader2 = Mockito.mock(CiphertextDirCache.CipherDirLoader.class); + + var result = cache.get(clearPath, dirLoader); + var result2 = cache.get(clearPath, dirLoader2); + Assertions.assertEquals(result2, result); + Mockito.verify(dirLoader2, Mockito.never()).load(); + } + + @Nested + public class RemovalTest { + + CryptoPath prefixPath = Mockito.mock(CryptoPath.class); + + @Test + public void entryRemovedOnPrefixSuccess() throws IOException { + Mockito.when(clearPath.startsWith(prefixPath)).thenReturn(true); + + cache.get(clearPath, dirLoader); //triggers loader + cache.removeAllKeysWithPrefix(prefixPath); + cache.get(clearPath, dirLoader); //triggers loader + + Mockito.verify(dirLoader, Mockito.times(2)).load(); + } + + @Test + public void entryStaysOnPrefixFailure() throws IOException { + Mockito.when(clearPath.startsWith(prefixPath)).thenReturn(false); + + cache.get(clearPath, dirLoader); //triggers loader + cache.removeAllKeysWithPrefix(prefixPath); + cache.get(clearPath, dirLoader); //does not trigger + + Mockito.verify(dirLoader).load(); + } + } + + + @Nested + public class RemapTest { + + CryptoPath newClearPath; + CryptoPath oldPrefixPath; + CryptoPath newPrefixPath; + + @BeforeEach + public void beforeEach() throws IOException { + newClearPath = Mockito.mock(CryptoPath.class); + oldPrefixPath = Mockito.mock(CryptoPath.class); + newPrefixPath = Mockito.mock(CryptoPath.class); + Mockito.when(oldPrefixPath.relativize(Mockito.any(Path.class))).thenReturn(oldPrefixPath); + Mockito.when(newPrefixPath.resolve(Mockito.any(Path.class))).thenReturn(newClearPath); + } + + @Test + public void entryRemappedOnPrefixSuccess() throws IOException { + Mockito.when(clearPath.startsWith(oldPrefixPath)).thenReturn(true); + + cache.get(clearPath, dirLoader); //triggers loader + cache.recomputeAllKeysWithPrefix(oldPrefixPath, newPrefixPath); + cache.get(clearPath, dirLoader); //does trigger + cache.get(newClearPath, dirLoader); //does not trigger + + Mockito.verify(dirLoader, Mockito.times(2)).load(); + } + + @Test + public void entryUntouchedOnPrefixFailure() throws IOException { + Mockito.when(clearPath.startsWith(oldPrefixPath)).thenReturn(false); + + cache.get(clearPath, dirLoader); //triggers loader + cache.recomputeAllKeysWithPrefix(oldPrefixPath, newPrefixPath); + cache.get(clearPath, dirLoader); //does not trigger + cache.get(newClearPath, dirLoader); //does trigger + + Mockito.verify(dirLoader, Mockito.times(2)).load(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java index 5095b5cd..3b7d8462 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java @@ -1,6 +1,5 @@ package org.cryptomator.cryptofs; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.attr.AttributeByNameProvider; import org.cryptomator.cryptofs.attr.AttributeProvider; import org.cryptomator.cryptofs.attr.AttributeViewProvider; @@ -1258,11 +1257,11 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException Path ciphertextDirPath = mock(Path.class, "d/FF/FF/"); CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext"); String dirId = "DirId1234ABC"; - CiphertextDirectory cipherDirObject = new CiphertextDirectory(dirId, ciphertextDirPath); + CiphertextDirectory ciphertextDirectoryObject = new CiphertextDirectory(dirId, ciphertextDirPath); FileChannelMock channel = new FileChannelMock(100); when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile); when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath); - when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(cipherDirObject); + when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(ciphertextDirectoryObject); when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("parentDirId", ciphertextParent)); when(cryptoPathMapper.getCiphertextFileType(path)).thenThrow(NoSuchFileException.class); when(ciphertextPath.getRawPath()).thenReturn(ciphertextRawPath); @@ -1278,7 +1277,7 @@ public void createDirectoryBackupsDirIdInCiphertextDirPath() throws IOException inTest.createDirectory(path); verify(readonlyFlag).assertWritable(); - verify(dirIdBackup, Mockito.times(1)).execute(cipherDirObject); + verify(dirIdBackup, Mockito.times(1)).execute(ciphertextDirectoryObject); } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java index 32241fb8..93cdda8d 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java @@ -64,6 +64,7 @@ public void setup() { Mockito.when(fileSystem.getEmptyPath()).thenReturn(empty); } + @Test public void testPathEncryptionForRoot() throws IOException { Path d00 = Mockito.mock(Path.class); @@ -74,7 +75,7 @@ public void testPathEncryptionForRoot() throws IOException { Mockito.when(d00.resolve("00")).thenReturn(d0000); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); - Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path; + Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path(); Assertions.assertEquals(d0000, path); } @@ -98,7 +99,7 @@ public void testPathEncryptionForFoo() throws IOException { Mockito.when(d00.resolve("01")).thenReturn(d0001); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); - Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path; + Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path(); Assertions.assertEquals(d0001, path); } @@ -132,7 +133,7 @@ public void testPathEncryptionForFooBar() throws IOException { Mockito.when(d00.resolve("02")).thenReturn(d0002); CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); - Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path; + Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path(); Assertions.assertEquals(d0002, path); } @@ -333,7 +334,113 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException { Assertions.assertEquals(CiphertextFileType.FILE, type); } + } + + @Nested + public class InvalidateOrMovePathMapping { + + Path d00 = Mockito.mock(Path.class); + Path d0000 = Mockito.mock(Path.class, "d/00/00"); + Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r"); + Path d0000oofdirFile = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r"); + Path d0001 = Mockito.mock(Path.class, "d/00/01"); + Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r"); + Path d0000rabdirFile = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r"); + Path d0002 = Mockito.mock(Path.class); + Path d0000kik = Mockito.mock(Path.class, "d/00/00/kik.c9r"); + Path d0000kikdirFile = Mockito.mock(Path.class, "d/00/00/kik.c9r/dir.c9r"); + Path d0003 = Mockito.mock(Path.class, "d/00/03/kik.c9r"); + + @BeforeEach + void beforeEach() throws IOException { + Mockito.when(dataRoot.resolve("00")).thenReturn(d00); + Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000"); + + // /foo + Mockito.when(d00.resolve("00")).thenReturn(d0000); + Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof); + Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdirFile); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof"); + Mockito.when(dirIdProvider.load(d0000oofdirFile)).thenReturn("1"); + Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001"); + + // /kik + Mockito.when(d0000.resolve("kik.c9r")).thenReturn(d0000kik); + Mockito.when(d0000kik.resolve("dir.c9r")).thenReturn(d0000kikdirFile); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("kik"), Mockito.any())).thenReturn("kik"); + Mockito.when(dirIdProvider.load(d0000kikdirFile)).thenReturn("3"); + Mockito.when(fileNameCryptor.hashDirectoryId("3")).thenReturn("0003"); + + // /foo/bar + Mockito.when(d00.resolve("01")).thenReturn(d0001); + Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab); + Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdirFile); + Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab"); + Mockito.when(dirIdProvider.load(d0000rabdirFile)).thenReturn("2"); + Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002"); + + Mockito.when(d00.resolve("02")).thenReturn(d0002); + Mockito.when(d00.resolve("03")).thenReturn(d0003); + } + + @Test + @DisplayName("Invalidating node causes cache miss on next retrieval") + public void testRemovedEntryMiss() throws IOException { + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + var fooPath = fileSystem.getPath("/foo"); + mapper.getCiphertextDir(fooPath); + mapper.invalidatePathMapping(fooPath); + var mapperSpy = Mockito.spy(mapper); + mapperSpy.getCiphertextDir(fooPath); + Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooPath); //loader is triggered, hence we have a cache miss + } + + @Test + @DisplayName("Invalidating node also invalidates all children") + public void testRemovedEntryChildMiss() throws IOException { + var fooPath = fileSystem.getPath("/foo"); + var fooBarPath = fileSystem.getPath("/foo/bar"); + + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + mapper.getCiphertextDir(fooPath); + mapper.getCiphertextDir(fooBarPath); + mapper.invalidatePathMapping(fooPath); + var mapperSpy = Mockito.spy(mapper); + mapperSpy.getCiphertextDir(fooBarPath); + Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooBarPath); //loader is triggered, hence we have a cache miss + mapperSpy.getCiphertextDir(fooBarPath); + } + + @Test + @DisplayName("Moving node causes cache miss for oldPath and cache hit for new") + public void testMoveEntryOldMissNewHit() throws IOException { + var fooPath = fileSystem.getPath("/foo"); + var kikPath = fileSystem.getPath("/kik"); + + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + mapper.getCiphertextDir(fooPath); + mapper.movePathMapping(fooPath, kikPath); + var mapperSpy = Mockito.spy(mapper); + mapperSpy.getCiphertextDir(fooPath); + Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooPath); //loader is triggered, hence we have a cache miss + Mockito.verify(mapperSpy, Mockito.never()).getCiphertextFilePath(kikPath); //loader is not triggered, hence we have a cache hit + } + @Test + @DisplayName("Moving node causes cache miss for childs of oldPath") + public void testMoveEntryOldChildMiss() throws IOException { + var fooPath = fileSystem.getPath("/foo"); + var fooBarPath = fileSystem.getPath("/foo/bar"); + var kikPath = fileSystem.getPath("/kik"); + + CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig); + mapper.getCiphertextDir(fooPath); + mapper.getCiphertextDir(fooBarPath); + mapper.movePathMapping(fooPath, kikPath); + var mapperSpy = Mockito.spy(mapper); + mapperSpy.getCiphertextDir(fooBarPath); + Mockito.verify(mapperSpy, Mockito.atLeastOnce()).getCiphertextFilePath(fooBarPath); //loader is triggered, hence we have a cache miss + } } } diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java index 19805b25..58739e90 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdBackupTest.java @@ -22,7 +22,7 @@ public class DirectoryIdBackupTest { Path contentPath; private String dirId = "12345678"; - private CryptoPathMapper.CiphertextDirectory cipherDirObject; + private CiphertextDirectory ciphertextDirectoryObject; private EncryptingWritableByteChannel encChannel; private Cryptor cryptor; @@ -31,7 +31,7 @@ public class DirectoryIdBackupTest { @BeforeEach public void init() { - cipherDirObject = new CryptoPathMapper.CiphertextDirectory(dirId, contentPath); + ciphertextDirectoryObject = new CiphertextDirectory(dirId, contentPath); cryptor = Mockito.mock(Cryptor.class); encChannel = Mockito.mock(EncryptingWritableByteChannel.class); @@ -44,7 +44,7 @@ public void testIdFileCreated() throws IOException { backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel); Mockito.when(encChannel.write(Mockito.any())).thenReturn(0); - dirIdBackup.execute(cipherDirObject); + dirIdBackup.execute(ciphertextDirectoryObject); Assertions.assertTrue(Files.exists(contentPath.resolve(Constants.DIR_BACKUP_FILE_NAME))); } @@ -58,7 +58,7 @@ public void testContentIsWritten() throws IOException { try (MockedStatic backupMock = Mockito.mockStatic(DirectoryIdBackup.class)) { backupMock.when(() -> DirectoryIdBackup.wrapEncryptionAround(Mockito.any(), Mockito.eq(cryptor))).thenReturn(encChannel); - dirIdBackup.execute(cipherDirObject); + dirIdBackup.execute(ciphertextDirectoryObject); Mockito.verify(encChannel, Mockito.times(1)).write(Mockito.argThat(b -> b.equals(expectedWrittenContent))); } diff --git a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java index d123f3df..33230151 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/AttributeProviderTest.java @@ -8,10 +8,10 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.CiphertextFilePath; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.cryptomator.cryptofs.Symlinks; import org.cryptomator.cryptofs.common.CiphertextFileType; import org.junit.jupiter.api.Assertions; diff --git a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java index e18810e1..c2bf66a9 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java @@ -1,5 +1,6 @@ package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.CryptoPathMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -31,7 +32,7 @@ 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)); + Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CiphertextDirectory("asd", targetDir)); Node unfiltered = new Node(dir.resolve("foo.c9r")); Stream result = brokenDirectoryFilter.process(unfiltered); @@ -45,7 +46,7 @@ public void testProcessNodeWithMissingTargetDir(@TempDir Path dir) throws IOExce 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)); + Mockito.when(cryptoPathMapper.resolveDirectory(Mockito.any())).thenReturn(new CiphertextDirectory("asd", targetDir)); Node unfiltered = new Node(dir.resolve("foo.c9r")); Stream result = brokenDirectoryFilter.process(unfiltered); diff --git a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java index c4361278..eada83a8 100644 --- a/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/dir/DirectoryStreamFactoryTest.java @@ -1,8 +1,8 @@ package org.cryptomator.cryptofs.dir; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.CryptoPath; import org.cryptomator.cryptofs.CryptoPathMapper; -import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java index 8a9aef15..10e9ba35 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingContentDirTest.java @@ -1,6 +1,6 @@ package org.cryptomator.cryptofs.health.dirid; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptolib.api.Cryptor; @@ -60,7 +60,7 @@ public void testFix() throws IOException { result.fix(pathToVault, cryptor); var expectedPath = pathToVault.resolve("d/ri/diculous-32-char-pseudo-hashhh"); - ArgumentMatcher cipherDirMatcher = obj -> obj.dirId.equals(dirId) && obj.path.endsWith(expectedPath); + ArgumentMatcher cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().endsWith(expectedPath); dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); var attr = Assertions.assertDoesNotThrow(() -> Files.readAttributes(expectedPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)); Assertions.assertTrue(attr.isDirectory()); diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java index 09aedef5..cb97d0b7 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/MissingDirIdBackupTest.java @@ -1,6 +1,6 @@ package org.cryptomator.cryptofs.health.dirid; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptolib.api.Cryptor; @@ -43,7 +43,7 @@ public void testFix() throws IOException { result.fix(pathToVault, cryptor); var expectedPath = pathToVault.resolve(cipherDir); - ArgumentMatcher cipherDirMatcher = obj -> obj.dirId.equals(dirId) && obj.path.isAbsolute() && obj.path.equals(expectedPath); + ArgumentMatcher cipherDirMatcher = obj -> obj.dirId().equals(dirId) && obj.path().isAbsolute() && obj.path().equals(expectedPath); dirIdBackupMock.verify(() -> DirectoryIdBackup.backupManually(Mockito.eq(cryptor), Mockito.argThat(cipherDirMatcher)), Mockito.times(1)); } } diff --git a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java index fcc5888e..9f9c93b2 100644 --- a/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java +++ b/src/test/java/org/cryptomator/cryptofs/health/dirid/OrphanDirTest.java @@ -1,7 +1,7 @@ package org.cryptomator.cryptofs.health.dirid; import com.google.common.io.BaseEncoding; -import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.CiphertextDirectory; import org.cryptomator.cryptofs.DirectoryIdBackup; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants; @@ -20,7 +20,6 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; import java.nio.file.FileAlreadyExistsException; @@ -327,15 +326,15 @@ public void testAdoptOrphanedUnshortened() throws IOException { String newClearName = "OliverTwist"; Files.writeString(oldCipherPath, expectedMsg, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); - Files.createDirectories(stepParentDir.path); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); + Files.createDirectories(stepParentDir.path()); - Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(BaseEncoding.base64Url(), newClearName, stepParentDir.dirId.getBytes(StandardCharsets.UTF_8)); + Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(BaseEncoding.base64Url(), newClearName, stepParentDir.dirId().getBytes(StandardCharsets.UTF_8)); var sha1 = Mockito.mock(MessageDigest.class); result.adoptOrphanedResource(oldCipherPath, newClearName, false, stepParentDir, fileNameCryptor, sha1); - Assertions.assertEquals(expectedMsg, Files.readString(stepParentDir.path.resolve("adopted.c9r"))); + Assertions.assertEquals(expectedMsg, Files.readString(stepParentDir.path().resolve("adopted.c9r"))); Assertions.assertTrue(Files.notExists(oldCipherPath)); } @@ -348,8 +347,8 @@ public void testAdoptOrphanedShortened() throws IOException { Files.createDirectories(oldCipherPath); Files.createFile(oldCipherPath.resolve("name.c9s")); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); - Files.createDirectories(stepParentDir.path); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); + Files.createDirectories(stepParentDir.path()); Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any()); try (var baseEncodingClass = Mockito.mockStatic(BaseEncoding.class)) { @@ -363,8 +362,8 @@ public void testAdoptOrphanedShortened() throws IOException { result.adoptOrphanedResource(oldCipherPath, newClearName, true, stepParentDir, fileNameCryptor, sha1); } - Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("adopted_shortened.c9s"))); - Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path.resolve("adopted_shortened.c9s/name.c9s"))); + Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("adopted_shortened.c9s"))); + Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path().resolve("adopted_shortened.c9s/name.c9s"))); Assertions.assertTrue(Files.notExists(oldCipherPath)); } @@ -376,8 +375,8 @@ public void testAdoptOrphanedShortenedMissingNameC9s() throws IOException { String newClearName = "TomSawyer"; Files.createDirectories(oldCipherPath); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); - Files.createDirectories(stepParentDir.path); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", pathToVault.resolve("d/22/2222")); + Files.createDirectories(stepParentDir.path()); Mockito.doReturn("adopted").when(fileNameCryptor).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any()); try (var baseEncodingClass = Mockito.mockStatic(BaseEncoding.class)) { @@ -391,8 +390,8 @@ public void testAdoptOrphanedShortenedMissingNameC9s() throws IOException { result.adoptOrphanedResource(oldCipherPath, newClearName, true, stepParentDir, fileNameCryptor, sha1); } - Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("adopted_shortened.c9s"))); - Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path.resolve("adopted_shortened.c9s/name.c9s"))); + Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("adopted_shortened.c9s"))); + Assertions.assertEquals("adopted.c9r", Files.readString(stepParentDir.path().resolve("adopted_shortened.c9s/name.c9s"))); Assertions.assertTrue(Files.notExists(oldCipherPath)); } @@ -410,7 +409,7 @@ public void testFixNoDirId() throws IOException { Files.createFile(orphan1); Files.createDirectories(orphan2); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); VaultConfig config = Mockito.mock(VaultConfig.class); Mockito.doReturn(170).when(config).getShorteningThreshold(); @@ -443,7 +442,7 @@ public void testFixContinuesOnNotRecoverableFilename() throws IOException { var dirId = Optional.of("trololo-id"); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); VaultConfig config = Mockito.mock(VaultConfig.class); Mockito.doReturn(170).when(config).getShorteningThreshold(); @@ -482,7 +481,7 @@ public void testFixWithDirId() throws IOException { var dirId = Optional.of("trololo-id"); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); VaultConfig config = Mockito.mock(VaultConfig.class); Mockito.doReturn(170).when(config).getShorteningThreshold(); @@ -525,8 +524,8 @@ public void testFixWithNonCryptomatorFiles() throws IOException { var dirId = Optional.of("trololo-id"); - CryptoPathMapper.CiphertextDirectory stepParentDir = new CryptoPathMapper.CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); - Files.createDirectories(stepParentDir.path); //needs to be created here, otherwise the Files.move(non-crypto-resource, stepparent) will fail + CiphertextDirectory stepParentDir = new CiphertextDirectory("aaaaaa", dataDir.resolve("22/2222")); + Files.createDirectories(stepParentDir.path()); //needs to be created here, otherwise the Files.move(non-crypto-resource, stepparent) will fail VaultConfig config = Mockito.mock(VaultConfig.class); Mockito.doReturn(170).when(config).getShorteningThreshold(); @@ -548,7 +547,7 @@ public void testFixWithNonCryptomatorFiles() throws IOException { Mockito.verify(resultSpy, Mockito.never()).adoptOrphanedResource(Mockito.eq(unrelated), Mockito.any(), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan1), Mockito.eq(lostName1), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); Mockito.verify(resultSpy, Mockito.times(1)).adoptOrphanedResource(Mockito.eq(orphan2), Mockito.eq(lostName2), Mockito.anyBoolean(), Mockito.eq(stepParentDir), Mockito.eq(fileNameCryptor), Mockito.any()); - Assertions.assertTrue(Files.exists(stepParentDir.path.resolve("unrelated.file"))); + Assertions.assertTrue(Files.exists(stepParentDir.path().resolve("unrelated.file"))); Assertions.assertTrue(Files.notExists(cipherOrphan)); }