diff --git a/main/ant-kit/assembly.xml b/main/ant-kit/assembly.xml index afea7ec94..4ca4dfd66 100644 --- a/main/ant-kit/assembly.xml +++ b/main/ant-kit/assembly.xml @@ -14,6 +14,12 @@ libs + + target/fixed-binaries + false + fixed-binaries + 755 + target/package false diff --git a/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index c694cedc2..3753d71d9 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -60,6 +60,16 @@ src/main/resources true + + fixed-binaries/** + + + + src/main/resources + false + + fixed-binaries/** + diff --git a/main/ant-kit/src/main/resources/build.xml b/main/ant-kit/src/main/resources/build.xml index 0ca9be55e..06dcfc257 100644 --- a/main/ant-kit/src/main/resources/build.xml +++ b/main/ant-kit/src/main/resources/build.xml @@ -50,6 +50,7 @@ + diff --git a/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x64 b/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x64 new file mode 100644 index 000000000..bffda959a Binary files /dev/null and b/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x64 differ diff --git a/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x86 b/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x86 new file mode 100644 index 000000000..805062d62 Binary files /dev/null and b/main/ant-kit/src/main/resources/fixed-binaries/linux-launcher-x86 differ diff --git a/main/ant-kit/src/main/resources/package/linux/postinst b/main/ant-kit/src/main/resources/package/linux/postinst new file mode 100644 index 000000000..e77fa6366 --- /dev/null +++ b/main/ant-kit/src/main/resources/package/linux/postinst @@ -0,0 +1,50 @@ +#!/bin/sh +# postinst script for APPLICATION_NAME +# +# see: dh_installdeb(1) + +set -e + +# summary of how this script can be called: +# * `configure' +# * `abort-upgrade' +# * `abort-remove' `in-favour' +# +# * `abort-remove' +# * `abort-deconfigure' `in-favour' +# `removing' +# +# for details, see http://www.debian.org/doc/debian-policy/ or +# the debian-policy package + +case "$1" in + configure) + echo Adding shortcut to the menu +SECONDARY_LAUNCHERS_INSTALL +APP_CDS_CACHE + xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop +FILE_ASSOCIATION_INSTALL + + rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME + if [ $(uname -m) = "x86_64" ]; then + mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME + else + mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME + fi + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +# dh_installdeb will replace this with shell code automatically +# generated by other debhelper scripts. + +#DEBHELPER# + +exit 0 diff --git a/main/ant-kit/src/main/resources/package/linux/spec b/main/ant-kit/src/main/resources/package/linux/spec new file mode 100644 index 000000000..b40f9224e --- /dev/null +++ b/main/ant-kit/src/main/resources/package/linux/spec @@ -0,0 +1,54 @@ +Summary: APPLICATION_SUMMARY +Name: APPLICATION_PACKAGE +Version: APPLICATION_VERSION +Release: 1 +License: APPLICATION_LICENSE_TYPE +Vendor: APPLICATION_VENDOR +Prefix: /opt +Provides: APPLICATION_PACKAGE +Requires: ld-linux.so.2 libX11.so.6 libXext.so.6 libXi.so.6 libXrender.so.1 libXtst.so.6 libasound.so.2 libc.so.6 libdl.so.2 libgcc_s.so.1 libm.so.6 libpthread.so.0 libthread_db.so.1 +Autoprov: 0 +Autoreq: 0 + +#avoid ARCH subfolder +%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm + +#comment line below to enable effective jar compression +#it could easily get your package size from 40 to 15Mb but +#build time will substantially increase and it may require unpack200/system java to install +%define __jar_repack %{nil} + +%description +APPLICATION_DESCRIPTION + +%prep + +%build + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot}/opt +cp -r %{_sourcedir}/APPLICATION_FS_NAME %{buildroot}/opt + +%files +APPLICATION_LICENSE_FILE +/opt/APPLICATION_FS_NAME + +%post +SECONDARY_LAUNCHERS_INSTALL +APP_CDS_CACHE +xdg-desktop-menu install --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop +FILE_ASSOCIATION_INSTALL +rm /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME +if [ $(uname -m) = "x86_64" ]; then + mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x64 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME +else + mv /opt/APPLICATION_FS_NAME/app/linux-launcher-x86 /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME +fi + +%preun +SECONDARY_LAUNCHERS_REMOVE +xdg-desktop-menu uninstall --novendor /opt/APPLICATION_FS_NAME/APPLICATION_LAUNCHER_FILENAME.desktop +FILE_ASSOCIATION_REMOVE + +%clean diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java index 5b9e81ade..f31431003 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java @@ -8,7 +8,7 @@ *******************************************************************************/ package org.cryptomator.crypto.engine; -abstract class CryptoException extends RuntimeException { +public abstract class CryptoException extends RuntimeException { public CryptoException() { super(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java index cb94a12fa..1226da4ed 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.cryptomator.crypto.engine; +import java.util.regex.Pattern; + /** * Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts, * otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption. @@ -22,12 +24,9 @@ public interface FilenameCryptor { String hashDirectoryId(String cleartextDirectoryId); /** - * Tests without an actual decryption attempt, if a name is a well-formed ciphertext. - * - * @param ciphertextName Filename in question - * @return true if the given name is likely to be a valid ciphertext + * @return A Pattern that can be used to test, if a name is a well-formed ciphertext. */ - boolean isEncryptedFilename(String ciphertextName); + Pattern encryptedNamePattern(); /** * @param cleartextName original filename including cleartext file extension diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java index 5db611548..4b977d44d 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; import javax.crypto.AEADBadTagException; import javax.crypto.SecretKey; @@ -25,6 +26,7 @@ import org.cryptomator.siv.SivMode; class FilenameCryptorImpl implements FilenameCryptor { private static final BaseNCodec BASE32 = new Base32(); + private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}"); private static final ThreadLocal SHA1 = new ThreadLocalSha1(); private static final ThreadLocal AES_SIV = new ThreadLocal() { @Override @@ -50,8 +52,8 @@ class FilenameCryptorImpl implements FilenameCryptor { } @Override - public boolean isEncryptedFilename(String ciphertextName) { - return BASE32.isInAlphabet(ciphertextName); + public Pattern encryptedNamePattern() { + return BASE32_PATTERN; } @Override diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java new file mode 100644 index 000000000..4f9f9f7c7 --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java @@ -0,0 +1,81 @@ +package org.cryptomator.filesystem.crypto; + +import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX; + +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ConflictResolver { + + private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class); + private static final int UUID_FIRST_GROUP_STRLEN = 8; + + private final Pattern encryptedNamePattern; + private final Function> nameDecryptor; + private final Function> nameEncryptor; + + public ConflictResolver(Pattern encryptedNamePattern, Function> nameDecryptor, Function> nameEncryptor) { + this.encryptedNamePattern = encryptedNamePattern; + this.nameDecryptor = nameDecryptor; + this.nameEncryptor = nameEncryptor; + } + + public File resolveIfNecessary(File file) { + Matcher m = encryptedNamePattern.matcher(StringUtils.removeEnd(file.name(), DIR_SUFFIX)); + if (m.matches()) { + // full match, use file as is + return file; + } else if (m.find(0)) { + // partial match, might be conflicting + return resolveConflict(file, m.toMatchResult()); + } else { + // no match, file not relevant + return file; + } + } + + private File resolveConflict(File conflictingFile, MatchResult matchResult) { + String ciphertext = matchResult.group(); + boolean isDirectory = conflictingFile.name().substring(matchResult.end()).startsWith(DIR_SUFFIX); + Optional cleartext = nameDecryptor.apply(ciphertext); + if (cleartext.isPresent()) { + Folder folder = conflictingFile.parent().get(); + File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext); + if (canonicalFile.exists()) { + // conflict detected! look for an alternative name: + File alternativeFile; + String conflictId; + do { + conflictId = createConflictId(); + String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")"; + String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get(); + alternativeFile = folder.file(isDirectory ? alternativeCiphertext + DIR_SUFFIX : alternativeCiphertext); + } while (alternativeFile.exists()); + LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile); + conflictingFile.moveTo(alternativeFile); + return alternativeFile; + } else { + conflictingFile.moveTo(canonicalFile); + return canonicalFile; + } + } else { + // not decryptable; false positive + return conflictingFile; + } + } + + private String createConflictId() { + return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN); + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java index 3f65e845c..ff0df04e7 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java @@ -8,8 +8,6 @@ *******************************************************************************/ package org.cryptomator.filesystem.crypto; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.io.UncheckedIOException; import java.nio.file.FileAlreadyExistsException; import java.util.Optional; @@ -27,9 +25,7 @@ class CryptoFile extends CryptoNode implements File { @Override protected Optional encryptedName() { - return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> { - return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId); - }); + return parent().get().encryptChildName(name()); } @Override diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index c8228535c..f83b1087f 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -18,35 +18,42 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.WeakValuedCache; import org.cryptomator.common.streams.AutoClosingStream; +import org.cryptomator.crypto.engine.CryptoException; import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.filesystem.Deleter; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; import org.cryptomator.io.FileContents; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class CryptoFolder extends CryptoNode implements Folder { + private static final Logger LOG = LoggerFactory.getLogger(CryptoFolder.class); private final WeakValuedCache folders = WeakValuedCache.usingLoader(this::newFolder); private final WeakValuedCache files = WeakValuedCache.usingLoader(this::newFile); private final AtomicReference directoryId = new AtomicReference<>(); + private final ConflictResolver conflictResolver; public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) { super(parent, name, cryptor); + this.conflictResolver = new ConflictResolver(cryptor.getFilenameCryptor().encryptedNamePattern(), this::decryptChildName, this::encryptChildName); } + /* ======================= name + directory id ======================= */ + @Override protected Optional encryptedName() { if (parent().isPresent()) { - return parent().get().getDirectoryId().map(s -> s.getBytes(UTF_8)).map(parentDirId -> { - return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_SUFFIX; - }); + return parent().get().encryptChildName(name()).map(s -> s + DIR_SUFFIX); } else { return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX); } @@ -73,24 +80,56 @@ class CryptoFolder extends CryptoNode implements Folder { })); } + /* ======================= children ======================= */ + @Override public Stream children() { return AutoClosingStream.from(Stream.concat(files(), folders())); } + private Stream nonConflictingFiles() { + final Stream files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty()); + return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary); + } + + private Predicate containsEncryptedName() { + final Pattern encryptedNamePattern = cryptor.getFilenameCryptor().encryptedNamePattern(); + return (File file) -> encryptedNamePattern.matcher(file.name()).find(); + } + + Optional decryptChildName(String ciphertextFileName) { + return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> { + try { + return cryptor.getFilenameCryptor().decryptFilename(ciphertextFileName, dirId); + } catch (CryptoException e) { + LOG.warn("Filename decryption of {} failed: {}", ciphertextFileName, e.getMessage()); + return null; + } + }); + } + + Optional encryptChildName(String cleartextFileName) { + return getDirectoryId().map(s -> s.getBytes(UTF_8)).map(dirId -> { + return cryptor.getFilenameCryptor().encryptFilename(cleartextFileName, dirId); + }); + } + @Override public Stream files() { - final Stream files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty()); - return files.map(File::name).filter(isEncryptedFileName()).map(this::decryptChildFileName).map(this::file); + return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file); } - private Predicate isEncryptedFileName() { - return (String name) -> !name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(name); + @Override + public Stream folders() { + return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix()).map(this::removeDirSuffix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder); } - private String decryptChildFileName(String encryptedFileName) { - final byte[] dirId = getDirectoryId().get().getBytes(UTF_8); - return cryptor.getFilenameCryptor().decryptFilename(encryptedFileName, dirId); + private Predicate endsWithDirSuffix() { + return (String encryptedFolderName) -> StringUtils.endsWith(encryptedFolderName, DIR_SUFFIX); + } + + private String removeDirSuffix(String encryptedFolderName) { + return StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX); } @Override @@ -98,35 +137,21 @@ class CryptoFolder extends CryptoNode implements Folder { return files.get(name); } - public CryptoFile newFile(String name) { - return new CryptoFile(this, name, cryptor); - } - - @Override - public Stream folders() { - final Stream files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty()); - return files.map(File::name).filter(isEncryptedDirectoryName()).map(this::decryptChildFolderName).map(this::folder); - } - - private Predicate isEncryptedDirectoryName() { - return (String name) -> name.endsWith(DIR_SUFFIX) && cryptor.getFilenameCryptor().isEncryptedFilename(StringUtils.removeEnd(name, DIR_SUFFIX)); - } - - private String decryptChildFolderName(String encryptedFolderName) { - final byte[] dirId = getDirectoryId().get().getBytes(UTF_8); - final String ciphertext = StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX); - return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId); - } - @Override public CryptoFolder folder(String name) { return folders.get(name); } - public CryptoFolder newFolder(String name) { + private CryptoFile newFile(String name) { + return new CryptoFile(this, name, cryptor); + } + + private CryptoFolder newFolder(String name) { return new CryptoFolder(this, name, cryptor); } + /* ======================= create/move/delete ======================= */ + @Override public void create() { parent.create(); @@ -176,7 +201,7 @@ class CryptoFolder extends CryptoNode implements Folder { // cut all ties: this.invalidateDirectoryIdsRecursively(); - assert!exists(); + assert !exists(); assert target.exists(); } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java index 9f3901f7e..590582ba8 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java @@ -12,6 +12,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.BaseNCodec; @@ -19,6 +20,7 @@ import org.apache.commons.codec.binary.BaseNCodec; class NoFilenameCryptor implements FilenameCryptor { private static final BaseNCodec BASE32 = new Base32(); + private static final Pattern WILDCARD_PATTERN = Pattern.compile(".*"); private static final ThreadLocal SHA1 = new ThreadLocalSha1(); @Override @@ -29,8 +31,8 @@ class NoFilenameCryptor implements FilenameCryptor { } @Override - public boolean isEncryptedFilename(String ciphertextName) { - return true; + public Pattern encryptedNamePattern() { + return WILDCARD_PATTERN; } @Override diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java new file mode 100644 index 000000000..10f8a4638 --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java @@ -0,0 +1,110 @@ +package org.cryptomator.filesystem.crypto; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.BaseNCodec; +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class ConflictResolverTest { + + private ConflictResolver conflictResolver; + private Folder folder; + private File canonicalFile; + private File canonicalFolder; + private File conflictingFile; + private File conflictingFolder; + private File resolved; + private File unrelatedFile; + + @Before + public void setup() { + Pattern base32Pattern = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}"); + BaseNCodec base32 = new Base32(); + Function> decode = (s) -> Optional.of(new String(base32.decode(s), StandardCharsets.UTF_8)); + Function> encode = (s) -> Optional.of(base32.encodeAsString(s.getBytes(StandardCharsets.UTF_8))); + conflictResolver = new ConflictResolver(base32Pattern, decode, encode); + + folder = Mockito.mock(Folder.class); + canonicalFile = Mockito.mock(File.class); + canonicalFolder = Mockito.mock(File.class); + conflictingFile = Mockito.mock(File.class); + conflictingFolder = Mockito.mock(File.class); + resolved = Mockito.mock(File.class); + unrelatedFile = Mockito.mock(File.class); + + String canonicalFileName = encode.apply("test name").get(); + String canonicalFolderName = canonicalFileName + Constants.DIR_SUFFIX; + String conflictingFileName = canonicalFileName + " (version 2)"; + String conflictingFolderName = canonicalFolderName + " (version 2)"; + String unrelatedName = "notBa$e32!"; + + Mockito.when(canonicalFile.name()).thenReturn(canonicalFileName); + Mockito.when(canonicalFolder.name()).thenReturn(canonicalFolderName); + Mockito.when(conflictingFile.name()).thenReturn(conflictingFileName); + Mockito.when(conflictingFolder.name()).thenReturn(conflictingFolderName); + Mockito.when(unrelatedFile.name()).thenReturn(unrelatedName); + + Mockito.when(canonicalFile.exists()).thenReturn(true); + Mockito.when(canonicalFolder.exists()).thenReturn(true); + Mockito.when(conflictingFile.exists()).thenReturn(true); + Mockito.when(conflictingFolder.exists()).thenReturn(true); + Mockito.when(unrelatedFile.exists()).thenReturn(true); + + Mockito.doReturn(Optional.of(folder)).when(canonicalFile).parent(); + Mockito.doReturn(Optional.of(folder)).when(canonicalFolder).parent(); + Mockito.doReturn(Optional.of(folder)).when(conflictingFile).parent(); + Mockito.doReturn(Optional.of(folder)).when(conflictingFolder).parent(); + Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent(); + + Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved); + Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile); + Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder); + Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile); + Mockito.when(folder.file(conflictingFolderName)).thenReturn(conflictingFolder); + Mockito.when(folder.file(unrelatedName)).thenReturn(unrelatedFile); + } + + @Test + public void testCanonicalName() { + File resolved = conflictResolver.resolveIfNecessary(canonicalFile); + Assert.assertSame(canonicalFile, resolved); + } + + @Test + public void testUnrelatedName() { + File resolved = conflictResolver.resolveIfNecessary(unrelatedFile); + Assert.assertSame(unrelatedFile, resolved); + } + + @Test + public void testConflictingFile() { + File resolved = conflictResolver.resolveIfNecessary(conflictingFile); + Mockito.verify(conflictingFile).moveTo(resolved); + Assert.assertSame(resolved, resolved); + } + + @Test + public void testConflictingFileIfCanonicalDoesnExist() { + Mockito.when(canonicalFile.exists()).thenReturn(false); + File resolved = conflictResolver.resolveIfNecessary(conflictingFile); + Mockito.verify(conflictingFile).moveTo(canonicalFile); + Assert.assertSame(canonicalFile, resolved); + } + + @Test + public void testConflictingFolder() { + File resolved = conflictResolver.resolveIfNecessary(conflictingFolder); + Mockito.verify(conflictingFolder).moveTo(resolved); + Assert.assertSame(resolved, resolved); + } + +} diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java index 93443d5ed..557e399eb 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java @@ -8,8 +8,6 @@ *******************************************************************************/ package org.cryptomator.filesystem.shortening; -import java.io.FileNotFoundException; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -19,9 +17,12 @@ import org.apache.commons.codec.binary.BaseNCodec; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.io.FileContents; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class FilenameShortener { + private static final Logger LOG = LoggerFactory.getLogger(FilenameShortener.class); private static final String LONG_NAME_FILE_EXT = ".lng"; private static final ThreadLocal SHA1 = new ThreadLocalSha1(); private static final BaseNCodec BASE32 = new Base32(); @@ -71,7 +72,8 @@ class FilenameShortener { private String loadMapping(String shortName) { final File mappingFile = mappingFile(shortName); if (!mappingFile.exists()) { - throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile)); + LOG.warn("Mapping file not found: " + mappingFile); + return shortName; } else { return FileContents.UTF_8.readContents(mappingFile); } diff --git a/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/FilenameShortenerTest.java b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/FilenameShortenerTest.java index 76d4c5b6b..faab82718 100644 --- a/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/FilenameShortenerTest.java +++ b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/FilenameShortenerTest.java @@ -8,8 +8,6 @@ *******************************************************************************/ package org.cryptomator.filesystem.shortening; -import java.io.UncheckedIOException; - import org.cryptomator.filesystem.FileSystem; import org.cryptomator.filesystem.inmem.InMemoryFileSystem; import org.junit.Assert; @@ -45,12 +43,12 @@ public class FilenameShortenerTest { Assert.assertEquals("short", shortener.inflate("short")); } - @Test(expected = UncheckedIOException.class) + @Test public void testInflateWithoutMappingFile() { FileSystem fs = new InMemoryFileSystem(); FilenameShortener shortener = new FilenameShortener(fs, 10); - shortener.inflate("iJustMadeThisNameUp.lng"); + Assert.assertEquals("iJustMadeThisNameUp.lng", shortener.inflate("iJustMadeThisNameUp.lng")); } } diff --git a/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ShorteningFileSystemTest.java b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ShorteningFileSystemTest.java index 6506b2b32..b4dad258c 100644 --- a/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ShorteningFileSystemTest.java +++ b/main/filesystem-nameshortening/src/test/java/org/cryptomator/filesystem/shortening/ShorteningFileSystemTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertThat; import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.concurrent.TimeoutException; @@ -102,6 +103,31 @@ public class ShorteningFileSystemTest { Assert.assertTrue(correspondingMetadataFile.exists()); } + @Test + public void testInflate() { + final FileSystem underlyingFs = new InMemoryFileSystem(); + final Folder metadataRoot = underlyingFs.folder(METADATA_DIR_NAME); + final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10); + final File correspondingMetadataFile = metadataRoot.folder("QM").folder("JL").file("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng"); + final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng"); + shortenedFolder.create(); + correspondingMetadataFile.parent().get().create(); + try (WritableFile w = correspondingMetadataFile.openWritable()) { + w.write(ByteBuffer.wrap("morethantenchars".getBytes(StandardCharsets.UTF_8))); + } + Assert.assertTrue(correspondingMetadataFile.exists()); + Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("morethantenchars"))); + } + + @Test + public void testInflateFailedDueToMissingMapping() { + final FileSystem underlyingFs = new InMemoryFileSystem(); + final FileSystem fs = new ShorteningFileSystem(underlyingFs, METADATA_DIR_NAME, 10); + final Folder shortenedFolder = underlyingFs.folder("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng"); + shortenedFolder.create(); + Assert.assertTrue(fs.folders().map(Folder::name).anyMatch(n -> n.equals("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng"))); + } + @Test public void testMoveLongFolders() { final FileSystem underlyingFs = new InMemoryFileSystem(); diff --git a/main/filesystem-nameshortening/src/test/resources/log4j2.xml b/main/filesystem-nameshortening/src/test/resources/log4j2.xml new file mode 100644 index 000000000..9b4889392 --- /dev/null +++ b/main/filesystem-nameshortening/src/test/resources/log4j2.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index 80cd60cab..7c1532775 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -43,6 +43,9 @@ public class SettingsController extends LocalizedFXMLViewController { @FXML private TextField portField; + @FXML + private Label useIpv6Label; + @FXML private CheckBox useIpv6Checkbox; @@ -55,7 +58,8 @@ public class SettingsController extends LocalizedFXMLViewController { checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled() && !areUpdatesManagedExternally()); portField.setText(String.valueOf(settings.getPort())); portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents); - useIpv6Checkbox.setDisable(!SystemUtils.IS_OS_WINDOWS); + useIpv6Label.setVisible(SystemUtils.IS_OS_WINDOWS); + useIpv6Checkbox.setVisible(SystemUtils.IS_OS_WINDOWS); useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); @@ -81,7 +85,7 @@ public class SettingsController extends LocalizedFXMLViewController { private void portDidChange(String newValue) { try { int port = Integer.parseInt(newValue); - if (port < Settings.MIN_PORT || port > Settings.MAX_PORT) { + if (!settings.isPortValid(port)) { settings.setPort(Settings.DEFAULT_PORT); } else { settings.setPort(port); diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 17f7c5b25..6b00aeb41 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -93,8 +93,8 @@ public class Settings implements Serializable { } } - private boolean isPortValid(int port) { - return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT; + public boolean isPortValid(int port) { + return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT || port == 0; } public boolean shouldUseIpv6() { diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index eebe54528..73f26630f 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -38,7 +38,7 @@ -