diff --git a/pom.xml b/pom.xml
index 85a7e458..0ec1c08a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 1.9.8
+ 1.9.9
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index 054d4e93..3cc1f046 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -640,9 +640,11 @@ CryptoPath getEmptyPath() {
}
void assertCiphertextPathLengthMeetsLimitations(Path cdrFilePath) throws FileNameTooLongException {
- String vaultRelativePath = pathToVault.relativize(cdrFilePath).toString();
- if (vaultRelativePath.length() > fileSystemProperties.maxPathLength()) {
- throw new FileNameTooLongException(vaultRelativePath, fileSystemProperties.maxPathLength());
+ Path vaultRelativePath = pathToVault.relativize(cdrFilePath);
+ String fileName = vaultRelativePath.getName(3).toString(); // fourth path element (d/xx/yyyyy/file.c9r/symlink.c9r)
+ String path = vaultRelativePath.toString();
+ if (fileName.length() > fileSystemProperties.maxNameLength() || path.length() > fileSystemProperties.maxPathLength()) {
+ throw new FileNameTooLongException(path, fileSystemProperties.maxPathLength(), fileSystemProperties.maxNameLength());
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 356a7f35..7329773d 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -44,7 +44,7 @@ public class CryptoFileSystemProperties extends AbstractMap {
public static final String PROPERTY_PASSPHRASE = "passphrase";
/**
- * Key identifying the pepper used during key derivation.
+ * Maximum ciphertext path length.
*
* @since 1.9.8
*/
@@ -52,6 +52,15 @@ public class CryptoFileSystemProperties extends AbstractMap {
static final int DEFAULT_MAX_PATH_LENGTH = Constants.MAX_CIPHERTEXT_PATH_LENGTH;
+ /**
+ * Maximum filename length of .c9r files.
+ *
+ * @since 1.9.9
+ */
+ public static final String PROPERTY_MAX_NAME_LENGTH = "maxNameLength";
+
+ static final int DEFAULT_MAX_NAME_LENGTH = Constants.MAX_CIPHERTEXT_NAME_LENGTH;
+
/**
* Key identifying the pepper used during key derivation.
*
@@ -124,7 +133,8 @@ private CryptoFileSystemProperties(Builder builder) {
entry(PROPERTY_PEPPER, builder.pepper), //
entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
- entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength) //
+ entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
+ entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) //
)));
}
@@ -160,6 +170,10 @@ String masterkeyFilename() {
int maxPathLength() {
return (int) get(PROPERTY_MAX_PATH_LENGTH);
}
+
+ int maxNameLength() {
+ return (int) get(PROPERTY_MAX_NAME_LENGTH);
+ }
@Override
public Set> entrySet() {
@@ -245,6 +259,7 @@ public static class Builder {
private final Set flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
private int maxPathLength = DEFAULT_MAX_PATH_LENGTH;
+ private int maxNameLength = DEFAULT_MAX_NAME_LENGTH;
private Builder() {
}
@@ -255,6 +270,7 @@ private Builder(Map properties) {
checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
checkedSet(Integer.class, PROPERTY_MAX_PATH_LENGTH, properties, this::withMaxPathLength);
+ checkedSet(Integer.class, PROPERTY_MAX_NAME_LENGTH, properties, this::withMaxNameLength);
}
private void checkedSet(Class type, String key, Map properties, Consumer setter) {
@@ -291,6 +307,18 @@ public Builder withMaxPathLength(int maxPathLength) {
return this;
}
+ /**
+ * Sets the maximum ciphertext filename length for a CryptoFileSystem.
+ *
+ * @param maxNameLength The maximum ciphertext filename length allowed
+ * @return this
+ * @since 1.9.9
+ */
+ public Builder withMaxNameLength(int maxNameLength) {
+ this.maxNameLength = maxNameLength;
+ return this;
+ }
+
/**
* Sets the pepper for a CryptoFileSystem.
*
diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
index b388c8a5..127798db 100644
--- a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
+++ b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
@@ -6,13 +6,13 @@
/**
* Indicates that an operation failed, as it would result in a ciphertext path that is too long for the underlying file system.
*
- * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedPathLength(Path)
+ * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedFileNameLength(Path)
* @since 1.9.8
*/
public class FileNameTooLongException extends FileSystemException {
- public FileNameTooLongException(String c9rPathRelativeToVaultRoot, int maxLength) {
- super(c9rPathRelativeToVaultRoot, null, "File path too long. Max ciphertext path name is " + maxLength);
+ public FileNameTooLongException(String c9rPathRelativeToVaultRoot, int maxPathLength, int maxNameLength) {
+ super(c9rPathRelativeToVaultRoot, null, "File name or path too long. Max ciphertext path name length is " + maxPathLength + ". Max ciphertext name is " + maxNameLength);
}
}
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index fac4c3a7..0ad683f8 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -12,11 +12,7 @@ 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_PATH_LENGTH = 268; // inclusive, beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
- 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";
@@ -25,6 +21,11 @@ public final class Constants {
public static final String CONTENTS_FILE_NAME = "contents.c9r";
public static final String INFLATED_FILE_NAME = "name.c9s";
+ 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 MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r
+ 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 int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
+ public static final int MAX_CIPHERTEXT_PATH_LENGTH = MAX_CIPHERTEXT_NAME_LENGTH + MAX_ADDITIONAL_PATH_LENGTH;
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
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
index f665918f..30ff3f6c 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -1,5 +1,6 @@
package org.cryptomator.cryptofs.common;
+import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.io.MoreFiles;
import com.google.common.io.RecursiveDeleteOption;
@@ -17,19 +18,18 @@
public class FileSystemCapabilityChecker {
private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
- private static final int MAX_PATH_LEN_REQUIRED = Constants.MAX_CIPHERTEXT_PATH_LENGTH;
- private static final int MIN_PATH_LEN_REQUIRED = 64;
- private static final String TMP_FS_CHECK_DIR = "temporary-filesystem-capability-check-dir"; // must have 41 chars!
public enum Capability {
/**
* File system allows read access
+ *
* @since 1.9.3
*/
READ_ACCESS,
/**
* File system allows write access
+ *
* @since 1.9.3
*/
WRITE_ACCESS,
@@ -50,6 +50,7 @@ public void assertAllCapabilities(Path pathToVault) throws MissingCapabilityExce
/**
* Checks whether the underlying filesystem allows reading the given dir.
+ *
* @param pathToVault Path to a vault's storage location
* @throws MissingCapabilityException if the check fails
* @since 1.9.3
@@ -64,6 +65,7 @@ public void assertReadAccess(Path pathToVault) throws MissingCapabilityException
/**
* Checks whether the underlying filesystem allows writing to the given dir.
+ *
* @param pathToVault Path to a vault's storage location
* @throws MissingCapabilityException if the check fails
* @since 1.9.3
@@ -80,35 +82,68 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio
deleteRecursivelySilently(checkDir);
}
}
-
- public int determineSupportedPathLength(Path pathToVault) {
- if (canHandlePathLength(pathToVault, MAX_PATH_LEN_REQUIRED)) {
- return MAX_PATH_LEN_REQUIRED;
- } else {
- return determineSupportedPathLength(pathToVault, MIN_PATH_LEN_REQUIRED, MAX_PATH_LEN_REQUIRED);
+
+ /**
+ * Determinse the number of chars a ciphertext filename (including its extension) is allowed to have inside a vault's d/XX/YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/
directory.
+ *
+ * @param pathToVault Path to the vault
+ * @return Number of chars a .c9r file is allowed to have
+ * @throws IOException If unable to perform this check
+ */
+ public int determineSupportedFileNameLength(Path pathToVault) throws IOException {
+ int subPathLength = Constants.MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/"
+ return determineSupportedFileNameLength(pathToVault.resolve("c"), subPathLength, Constants.MIN_CIPHERTEXT_NAME_LENGTH, Constants.MAX_CIPHERTEXT_NAME_LENGTH);
+ }
+
+ /**
+ * Determines the number of chars a filename is allowed to have inside of subdirectories of dir
by running an experiment.
+ *
+ * @param dir Path to a directory where to conduct the experiment (e.g. /path/to/vault/c
)
+ * @param subPathLength Defines the combined number of chars of the subdirectories inside dir
, including slashes but excluding the leading slash. Must be a minimum of 6
+ * @param minFileNameLength The minimum filename length to check
+ * @param maxFileNameLength The maximum filename length to check
+ * @return The supported filename length inside a subdirectory of dir
with subPathLength
chars
+ * @throws IOException If unable to perform this check
+ */
+ public int determineSupportedFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException {
+ Preconditions.checkArgument(subPathLength >= 6, "subPathLength must be larger than charcount(a/nnn/)");
+ Preconditions.checkArgument(minFileNameLength > 0);
+ Preconditions.checkArgument(maxFileNameLength <= 999);
+
+ String fillerName = Strings.repeat("a", subPathLength - 5);
+ assert fillerName.length() > 0;
+ Path fillerDir = dir.resolve(fillerName);
+ try {
+ // make sure we can create _and_ see directories inside of checkDir:
+ Files.createDirectories(fillerDir.resolve("nnn"));
+ if (!canListDir(fillerDir)) {
+ throw new IOException("Unable to read dir");
+ }
+ // perform actual check:
+ return determineSupportedFileNameLength(fillerDir, minFileNameLength, maxFileNameLength + 1);
+ } finally {
+ deleteRecursivelySilently(fillerDir);
}
}
-
- private int determineSupportedPathLength(Path pathToVault, int lowerBound, int upperBound) {
- assert lowerBound <= upperBound;
- int mid = (lowerBound + upperBound) / 2;
- if (mid == lowerBound) {
+
+ private int determineSupportedFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) {
+ assert lowerBoundIncl < upperBoundExcl;
+ int mid = (lowerBoundIncl + upperBoundExcl) / 2;
+ assert mid < upperBoundExcl;
+ if (mid == lowerBoundIncl) {
return mid; // bounds will not shrink any further at this point
}
- if (canHandlePathLength(pathToVault, mid)) {
- return determineSupportedPathLength(pathToVault, mid, upperBound);
+ assert lowerBoundIncl < mid;
+ if (canHandleFileNameLength(p, mid)) {
+ return determineSupportedFileNameLength(p, mid, upperBoundExcl);
} else {
- return determineSupportedPathLength(pathToVault, lowerBound, mid);
+ return determineSupportedFileNameLength(p, lowerBoundIncl, mid);
}
}
-
- private boolean canHandlePathLength(Path pathToVault, int pathLength) {
- assert pathLength > 48;
- String checkDirStr = "c/" + TMP_FS_CHECK_DIR + String.format("/%03d/", pathLength);
- assert checkDirStr.length() == 48; // 268 - 220
- int filenameLength = pathLength - checkDirStr.length();
- Path checkDir = pathToVault.resolve(checkDirStr);
- Path checkFile = checkDir.resolve(Strings.repeat("a", filenameLength));
+
+ private boolean canHandleFileNameLength(Path parent, int nameLength) {
+ Path checkDir = parent.resolve(String.format("%03d", nameLength));
+ Path checkFile = checkDir.resolve(Strings.repeat("a", nameLength));
try {
Files.createDirectories(checkDir);
try {
@@ -116,18 +151,24 @@ private boolean canHandlePathLength(Path pathToVault, int pathLength) {
} catch (FileAlreadyExistsException e) {
// ok
}
- try (DirectoryStream ds = Files.newDirectoryStream(checkDir)) {
- ds.iterator().hasNext(); // will fail with DirectoryIteratorException on Windows if path of children too long
- return true;
- }
- } catch (DirectoryIteratorException | IOException e) {
+ return canListDir(checkDir); // will fail on Windows, if checkFile's name is too long
+ } catch (IOException e) {
return false;
} finally {
deleteSilently(checkFile); // despite not being able to dirlist, we might still be able to delete this
deleteRecursivelySilently(checkDir); // only works if dirlist works, therefore after deleting checkFile
}
}
-
+
+ private boolean canListDir(Path dir) {
+ try (DirectoryStream ds = Files.newDirectoryStream(dir)) {
+ ds.iterator().hasNext(); // throws DirectoryIteratorException on Windows if child path too long
+ return true;
+ } catch (DirectoryIteratorException | IOException e) {
+ return false;
+ }
+ }
+
private void deleteSilently(Path path) {
try {
Files.delete(path);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/VaultStatsVisitor.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/VaultStatsVisitor.java
index 7a8b4fd6..aa932cc2 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/VaultStatsVisitor.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/VaultStatsVisitor.java
@@ -1,6 +1,5 @@
package org.cryptomator.cryptofs.migration.v7;
-import org.cryptomator.cryptofs.common.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,14 +11,16 @@
import java.util.Optional;
public class VaultStatsVisitor extends SimpleFileVisitor {
-
+
private static final Logger LOG = LoggerFactory.getLogger(VaultStatsVisitor.class);
private final Path vaultRoot;
private final boolean determineMaxCiphertextPathLength;
private long fileCount = 0;
+ private long maxNameLength = 0;
private long maxPathLength = 0;
- private Path longestNewFile = null;
+ private Path pathWithLongestName = null;
+ private Path longestPath = null;
public VaultStatsVisitor(Path vaultRoot, boolean determineMaxCiphertextPathLength) {
this.vaultRoot = vaultRoot;
@@ -30,19 +31,30 @@ public long getTotalFileCount() {
return fileCount;
}
-
- public Path getLongestNewFile() {
- return longestNewFile;
+ public long getMaxCiphertextNameLength() {
+ if (determineMaxCiphertextPathLength) {
+ return maxNameLength;
+ } else {
+ return 220;
+ }
}
public long getMaxCiphertextPathLength() {
if (determineMaxCiphertextPathLength) {
return maxPathLength;
} else {
- return Constants.MAX_CIPHERTEXT_PATH_LENGTH;
+ return 268;
}
}
+ public Path getPathWithLongestName() {
+ return pathWithLongestName;
+ }
+
+ public Path getLongestPath() {
+ return longestPath;
+ }
+
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
fileCount++;
@@ -62,10 +74,16 @@ private void updateMaxCiphertextPathLength(FilePathMigration filePathMigration)
try {
Path newPath = filePathMigration.getTargetPath("");
Path relativeToVaultRoot = vaultRoot.relativize(newPath);
- int len = relativeToVaultRoot.toString().length();
- if (len > maxPathLength) {
- maxPathLength = len;
- longestNewFile = newPath;
+ int pathLen = relativeToVaultRoot.toString().length();
+ if (pathLen > maxPathLength) {
+ maxPathLength = pathLen;
+ longestPath = newPath;
+ }
+ String name = relativeToVaultRoot.getName(3).toString();
+ int nameLen = name.length();
+ if (nameLen > maxNameLength) {
+ maxNameLength = nameLen;
+ pathWithLongestName = newPath;
}
} catch (InvalidOldFilenameException e) {
LOG.warn("Encountered malformed filename.", e);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index f8adc156..e3b4f6cd 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -6,10 +6,10 @@
package org.cryptomator.cryptofs.migration.v7;
import org.cryptomator.cryptofs.FileNameTooLongException;
-import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
-import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
+import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationEvent;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
@@ -57,13 +57,14 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp
LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
// check file system capabilities:
- int pathLengthLimit = new FileSystemCapabilityChecker().determineSupportedPathLength(vaultRoot);
+ int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(vaultRoot.resolve("c"), 46, 28, 220);
+ int pathLengthLimit = filenameLengthLimit + 48; // TODO
VaultStatsVisitor vaultStats;
- if (pathLengthLimit >= Constants.MAX_CIPHERTEXT_PATH_LENGTH) {
- LOG.info("Underlying file system meets path length requirements.");
+ if (filenameLengthLimit >= 220) {
+ LOG.info("Underlying file system meets filename length requirements.");
vaultStats = new VaultStatsVisitor(vaultRoot, false);
} else {
- LOG.warn("Underlying file system only supports paths with up to {} chars (required: {}). Asking for user feedback...", pathLengthLimit, Constants.MAX_CIPHERTEXT_PATH_LENGTH);
+ LOG.warn("Underlying file system only supports names with up to {} chars (required: 220). Asking for user feedback...", filenameLengthLimit);
ContinuationResult result = continuationListener.continueMigrationOnEvent(ContinuationEvent.REQUIRES_FULL_VAULT_DIR_SCAN);
switch (result) {
case PROCEED:
@@ -80,13 +81,19 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp
// dry-run to collect stats:
Path dataDir = vaultRoot.resolve("d");
Files.walkFileTree(dataDir, EnumSet.noneOf(FileVisitOption.class), 3, vaultStats);
-
- // fail if file names are too long:
+
+ // fail if ciphertext paths are too long:
if (vaultStats.getMaxCiphertextPathLength() > pathLengthLimit) {
- LOG.error("Migration aborted due to lacking capabilities of underlying file system. Vault is unchanged.");
- throw new FileNameTooLongException(vaultStats.getLongestNewFile().toString(), pathLengthLimit);
+ LOG.error("Migration aborted due to unsupported path length (required {}) of underlying file system (supports {}). Vault is unchanged.", vaultStats.getMaxCiphertextPathLength(), pathLengthLimit);
+ throw new FileNameTooLongException(vaultStats.getLongestPath().toString(), pathLengthLimit, filenameLengthLimit);
}
-
+
+ // fail if ciphertext names are too long:
+ if (vaultStats.getMaxCiphertextNameLength() > filenameLengthLimit) {
+ LOG.error("Migration aborted due to unsupported filename length (required {}) of underlying file system (supports {}). Vault is unchanged.", vaultStats.getMaxCiphertextNameLength(), filenameLengthLimit);
+ throw new FileNameTooLongException(vaultStats.getPathWithLongestName().toString(), pathLengthLimit, filenameLengthLimit);
+ }
+
// start migration:
long toBeMigrated = vaultStats.getTotalFileCount();
LOG.info("Starting migration of {} files", toBeMigrated);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 2aa87113..670694d3 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -6,6 +6,7 @@
import org.cryptomator.cryptofs.attr.AttributeViewProvider;
import org.cryptomator.cryptofs.attr.AttributeViewType;
import org.cryptomator.cryptofs.common.CiphertextFileType;
+import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.FinallyUtil;
import org.cryptomator.cryptofs.common.RunnableThrowingException;
import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter;
@@ -115,7 +116,9 @@ public void setup() {
Path other = invocation.getArgument(0);
return other;
});
- when(fileSystemProperties.maxPathLength()).thenReturn(Integer.MAX_VALUE);
+
+ when(fileSystemProperties.maxPathLength()).thenReturn(Constants.MAX_CIPHERTEXT_PATH_LENGTH);
+ when(fileSystemProperties.maxNameLength()).thenReturn(Constants.MAX_CIPHERTEXT_NAME_LENGTH);
inTest = new CryptoFileSystemImpl(provider, cryptoFileSystems, pathToVault, cryptor,
fileStore, stats, cryptoPathMapper, cryptoPathFactory,
@@ -352,7 +355,7 @@ public void testNewWatchServiceThrowsUnsupportedOperationException() throws IOEx
public class NewFileChannel {
private final CryptoPath cleartextPath = mock(CryptoPath.class, "cleartext");
- private final CryptoPath ciphertextFilePath = mock(CryptoPath.class, "ciphertext");
+ private final CryptoPath ciphertextFilePath = mock(CryptoPath.class, "d/00/00/path.c9r");
private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class);
private final OpenCryptoFile openCryptoFile = mock(OpenCryptoFile.class);
private final FileChannel fileChannel = mock(FileChannel.class);
@@ -363,6 +366,7 @@ public void setup() throws IOException {
when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath);
when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath);
when(openCryptoFiles.getOrCreate(ciphertextFilePath)).thenReturn(openCryptoFile);
+ when(ciphertextFilePath.getName(3)).thenReturn(mock(CryptoPath.class, "path.c9r"));
when(openCryptoFile.newFileChannel(any())).thenReturn(fileChannel);
}
@@ -498,6 +502,7 @@ public class CopyAndMove {
private final Path ciphertextSourceDirFile = mock(Path.class, "d/00/00/source.c9r/dir.c9r");
private final Path ciphertextSourceDir = mock(Path.class, "d/00/SOURCE/");
private final Path ciphertextDestinationFile = mock(Path.class, "d/00/00/dest.c9r");
+ private final Path ciphertextDestinationFileName = mock(Path.class, "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/");
@@ -523,6 +528,8 @@ public void setup() throws IOException {
when(ciphertextDestinationDirFile.getFileSystem()).thenReturn(physicalFs);
when(ciphertextDestinationDir.getFileSystem()).thenReturn(physicalFs);
when(physicalFs.provider()).thenReturn(physicalFsProv);
+ when(ciphertextDestinationFile.getName(3)).thenReturn(ciphertextDestinationFileName);
+ when(ciphertextDestinationDirFile.getName(3)).thenReturn(ciphertextDestinationFileName);
when(cryptoPathMapper.getCiphertextFilePath(cleartextSource)).thenReturn(ciphertextSource);
when(cryptoPathMapper.getCiphertextFilePath(cleartextDestination)).thenReturn(ciphertextDestination);
when(cryptoPathMapper.getCiphertextDir(cleartextSource)).thenReturn(new CiphertextDirectory("foo", ciphertextSourceDir));
@@ -1003,6 +1010,7 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
+ when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);
inTest.createDirectory(path);
@@ -1034,6 +1042,7 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirFile.getFileSystem()).thenReturn(fileSystem);
when(ciphertextDirPath.getFileSystem()).thenReturn(fileSystem);
+ when(ciphertextDirFile.getName(3)).thenReturn(mock(Path.class, "path.c9r"));
when(provider.newFileChannel(ciphertextDirFile, EnumSet.of(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE))).thenReturn(channel);
// make createDirectory with an FileSystemException during Files.createDirectories(ciphertextDirPath)
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 7bb508a8..c556af23 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -15,10 +15,12 @@
import java.util.Map.Entry;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MASTERKEY_FILENAME;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_NAME_LENGTH;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_PATH_LENGTH;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_PEPPER;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_FILESYSTEM_FLAGS;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MASTERKEY_FILENAME;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_NAME_LENGTH;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_PATH_LENGTH;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PASSPHRASE;
import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PEPPER;
@@ -56,6 +58,7 @@ public void testSetOnlyPassphrase() {
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
@@ -79,6 +82,7 @@ public void testSetPassphraseAndReadonlyFlag() {
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
}
@@ -104,6 +108,7 @@ public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
}
@@ -117,7 +122,8 @@ public void testFromMap() {
map.put(PROPERTY_PASSPHRASE, passphrase);
map.put(PROPERTY_PEPPER, pepper);
map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
- map.put(PROPERTY_MAX_PATH_LENGTH, 255);
+ map.put(PROPERTY_MAX_PATH_LENGTH, 1000);
+ map.put(PROPERTY_MAX_NAME_LENGTH, 255);
map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build();
@@ -125,12 +131,15 @@ public void testFromMap() {
MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
MatcherAssert.assertThat(inTest.readonly(), is(true));
MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
+ MatcherAssert.assertThat(inTest.maxPathLength(), is(1000));
+ MatcherAssert.assertThat(inTest.maxNameLength(), is(255));
MatcherAssert.assertThat(inTest.entrySet(),
containsInAnyOrder( //
anEntry(PROPERTY_PASSPHRASE, passphrase), //
anEntry(PROPERTY_PEPPER, pepper), //
anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
- anEntry(PROPERTY_MAX_PATH_LENGTH, 255), //
+ anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, 255), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
}
@@ -157,6 +166,7 @@ public void testWrapMapWithTrueReadonly() {
anEntry(PROPERTY_PEPPER, pepper), //
anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
}
@@ -183,6 +193,7 @@ public void testWrapMapWithFalseReadonly() {
anEntry(PROPERTY_PEPPER, pepper), //
anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))));
}
@@ -243,6 +254,7 @@ public void testWrapMapWithoutReadonly() {
anEntry(PROPERTY_PEPPER, pepper), //
anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
+ anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
}
diff --git a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
index 99c56ef9..3a58f8f0 100644
--- a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
@@ -4,6 +4,8 @@
import org.cryptomator.cryptofs.mocks.SeekableByteChannelMock;
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;
@@ -23,18 +25,28 @@ class FileSystemCapabilityCheckerTest {
class PathLengthLimits {
private Path pathToVault = Mockito.mock(Path.class);
+ private Path cDir = Mockito.mock(Path.class);
+ private Path fillerDir = Mockito.mock(Path.class);
+ private Path nnnDir = Mockito.mock(Path.class);
private FileSystem fileSystem = Mockito.mock(FileSystem.class);
private FileSystemProvider fileSystemProvider = Mockito.mock(FileSystemProvider.class);
- @BeforeAll
- public void setup() {
+ @BeforeEach
+ public void setup() throws IOException {
Mockito.when(pathToVault.getFileSystem()).thenReturn(fileSystem);
Mockito.when(fileSystem.provider()).thenReturn(fileSystemProvider);
+ Mockito.when(pathToVault.resolve("c")).thenReturn(cDir);
+ Mockito.when(cDir.resolve(Mockito.anyString())).thenReturn(fillerDir);
+ Mockito.when(fillerDir.resolve(Mockito.anyString())).thenReturn(nnnDir);
+ Mockito.when(fillerDir.getFileSystem()).thenReturn(fileSystem);
+ Mockito.when(nnnDir.getFileSystem()).thenReturn(fileSystem);
+ Mockito.when(fileSystemProvider.newDirectoryStream(Mockito.eq(fillerDir), Mockito.any())).thenReturn(DirectoryStreamMock.empty());
}
@Test
- public void testDetermineSupportedPathLengthWithUnlimitedPathLength() {
- Mockito.when(pathToVault.resolve(Mockito.anyString())).then(invocation -> {
+ @DisplayName("determineSupportedFileNameLength() on unrestricted file system")
+ public void testUnlimitedLength() throws IOException {
+ Mockito.when(fillerDir.resolve(Mockito.anyString())).then(invocation -> {
String checkDirStr = invocation.getArgument(0);
Path checkDirMock = Mockito.mock(Path.class, checkDirStr);
Mockito.when(checkDirMock.getFileSystem()).thenReturn(fileSystem);
@@ -50,15 +62,16 @@ public void testDetermineSupportedPathLengthWithUnlimitedPathLength() {
return checkDirMock;
});
- int determinedLimit = new FileSystemCapabilityChecker().determineSupportedPathLength(pathToVault);
+ int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
- Assertions.assertEquals(268, determinedLimit);
+ Assertions.assertEquals(220, determinedLimit);
}
@Test
- public void testDetermineSupportedPathLengthWithLimitedPathLength() {
- int limit = 255;
- Mockito.when(pathToVault.resolve(Mockito.anyString())).then(invocation -> {
+ @DisplayName("determineSupportedFileNameLength() on restricted file system that allows file creation but fails in dir listing (Win/WebDAV)")
+ public void testLimitedLengthDuringDirListing() throws IOException {
+ int limit = 150;
+ Mockito.when(fillerDir.resolve(Mockito.anyString())).then(invocation -> {
String checkDirStr = invocation.getArgument(0);
Path checkDirMock = Mockito.mock(Path.class, checkDirStr);
Mockito.when(checkDirMock.getFileSystem()).thenReturn(fileSystem);
@@ -72,7 +85,7 @@ public void testDetermineSupportedPathLengthWithLimitedPathLength() {
});
Mockito.when(fileSystemProvider.newDirectoryStream(Mockito.eq(checkDirMock), Mockito.any())).then(invocation3 -> {
Iterable iterable = Mockito.mock(Iterable.class);
- if (Integer.valueOf(checkDirStr.substring(44, 47)) > limit) {
+ if (Integer.valueOf(checkDirStr) > limit) {
Mockito.when(iterable.iterator()).thenThrow(new DirectoryIteratorException(new IOException("path too long")));
} else {
Mockito.when(iterable.iterator()).thenReturn(Collections.emptyIterator());
@@ -82,15 +95,16 @@ public void testDetermineSupportedPathLengthWithLimitedPathLength() {
return checkDirMock;
});
- int determinedLimit = new FileSystemCapabilityChecker().determineSupportedPathLength(pathToVault);
+ int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
Assertions.assertEquals(limit, determinedLimit);
}
@Test
- public void testDetermineSupportedPathLengthWithLimitedNameLength() {
+ @DisplayName("determineSupportedFileNameLength() on restricted file system that fails during file creation (Linux/eCryptfs)")
+ public void testLimitedLengthDuringFileCreation() throws IOException {
int limit = 150;
- Mockito.when(pathToVault.resolve(Mockito.anyString())).then(invocation -> {
+ Mockito.when(fillerDir.resolve(Mockito.anyString())).then(invocation -> {
String checkDirStr = invocation.getArgument(0);
Path checkDirMock = Mockito.mock(Path.class, checkDirStr);
Mockito.when(checkDirMock.getFileSystem()).thenReturn(fileSystem);
@@ -98,7 +112,7 @@ public void testDetermineSupportedPathLengthWithLimitedNameLength() {
String checkFileStr = invocation.getArgument(0);
Path checkFileMock = Mockito.mock(Path.class, checkFileStr);
Mockito.when(checkFileMock.getFileSystem()).thenReturn(fileSystem);
- if (Integer.valueOf(checkDirStr.substring(44, 47)) > limit) {
+ if (Integer.valueOf(checkDirStr) > limit) {
Mockito.when(fileSystemProvider.newByteChannel(Mockito.eq(checkFileMock), Mockito.any()))
.thenThrow(new IOException("name too long"));
} else {
@@ -111,7 +125,7 @@ public void testDetermineSupportedPathLengthWithLimitedNameLength() {
return checkDirMock;
});
- int determinedLimit = new FileSystemCapabilityChecker().determineSupportedPathLength(pathToVault);
+ int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
Assertions.assertEquals(limit, determinedLimit);
}