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); }