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 extends Node> children() {
return AutoClosingStream.from(Stream.concat(files(), folders()));
}
+ private Stream nonConflictingFiles() {
+ final Stream extends File> 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 extends File> 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 extends File> 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 @@
-
+
diff --git a/main/ui/src/main/resources/localization/de.txt b/main/ui/src/main/resources/localization/de.txt
index d656e86c0..31dc2757c 100644
--- a/main/ui/src/main/resources/localization/de.txt
+++ b/main/ui/src/main/resources/localization/de.txt
@@ -3,6 +3,8 @@
# See the LICENSE.txt file for more info.
#
# Contributors:
+# Christian Schmickler
+# Joscha Feth
# Markus Kreusch
# Michael Schmetter
# Sebastian Wiesendahl
@@ -86,3 +88,8 @@ tray.menu.quit = Beenden
tray.infoMsg.title = Cryptomator läuft noch
tray.infoMsg.msg = Cryptomator läuft noch. Mit dem Tray-Icon beenden.
tray.infoMsg.msg.osx = Cryptomator läuft noch. Über die Menüleiste beenden.
+initialize.messageLabel.passwordStrength.0 = Sehr schwach
+initialize.messageLabel.passwordStrength.1 = Schwach
+initialize.messageLabel.passwordStrength.2 = Mittel
+initialize.messageLabel.passwordStrength.3 = Stark
+initialize.messageLabel.passwordStrength.4 = Sehr stark
\ No newline at end of file
diff --git a/main/ui/src/main/resources/localization/kr.txt b/main/ui/src/main/resources/localization/kr.txt
index 1978f9093..0970719ff 100644
--- a/main/ui/src/main/resources/localization/kr.txt
+++ b/main/ui/src/main/resources/localization/kr.txt
@@ -82,3 +82,8 @@ tray.menu.quit = 종료
tray.infoMsg.title = 계속 실행 중입니다.
tray.infoMsg.msg = Cryptomator가 계속 실행 중입니다. 종료하실려면 트레이 아이콘에서 해주세요.
tray.infoMsg.msg.osx = Cryptomator가 계속 실행중입니다. 종료하실려면 메뉴 바 아이콘에서 해주세요.
+initialize.messageLabel.passwordStrength.0 = 너무 약함
+initialize.messageLabel.passwordStrength.1 = 약함
+initialize.messageLabel.passwordStrength.2 = 괜찮음
+initialize.messageLabel.passwordStrength.3 = 강력함
+initialize.messageLabel.passwordStrength.4 = 매우 강력함
diff --git a/main/ui/src/main/resources/localization/sk.txt b/main/ui/src/main/resources/localization/sk.txt
index 74ef6bf14..371e19b4f 100644
--- a/main/ui/src/main/resources/localization/sk.txt
+++ b/main/ui/src/main/resources/localization/sk.txt
@@ -6,9 +6,6 @@
# Filip Havrlent
# Tatiana Chovancová
-# Copyright (c) 2016 The Cryptomator Contributors
-# This file is licensed under the terms of the MIT license.
-# See the LICENSE.txt file for more info.
app.name = Cryptomator
# main.fxml
main.emptyListInstructions = Pridať trezor
diff --git a/main/ui/src/main/resources/localization/tr.txt b/main/ui/src/main/resources/localization/tr.txt
new file mode 100644
index 000000000..31918f4df
--- /dev/null
+++ b/main/ui/src/main/resources/localization/tr.txt
@@ -0,0 +1,89 @@
+# Copyright (c) 2016 The Cryptomator Contributors
+# This file is licensed under the terms of the MIT license.
+# See the LICENSE.txt file for more info.
+#
+# Contributors:
+# Cem KOÇ
+
+app.name = Cryptomator
+# main.fxml
+main.emptyListInstructions = Kasa eklemek için tıkla
+main.directoryList.contextMenu.remove = Listeden sil
+main.directoryList.contextMenu.changePassword = Şifreyi değiştir
+main.addDirectory.contextMenu.new = Yeni bir kasa yarat
+main.addDirectory.contextMenu.open = Var olan kasayı aç
+# welcome.fxml
+welcome.checkForUpdates.label.currentlyChecking = Güncellemeler kontrol ediliyor...
+welcome.newVersionMessage = Sürüm %s indirilebilir. Şu anki sürüm\: %s
+# initialize.fxml
+initialize.label.password = Şifre
+initialize.label.retypePassword = Şifre (tekrar)
+initialize.button.ok = Kasa oluştur
+initialize.messageLabel.alreadyInitialized = Kasa çoktan başlatıldı
+initialize.messageLabel.initializationFailed = Kasa başlatılamadı. Detaylar için log dosyasına bakın.
+# notfound.fxml
+notfound.label = Kasa bulunamadı. Yeri değişmiş olabilir mi ?
+# upgrade.fxml
+upgrade.button = Kasayı yükselt.
+upgrade.version3dropBundleExtension.msg = Bu kasanın yeni formata geçirilmesi gerekmekte. "%1$s" ismi "%2$s" olarak değiştirilecek. Devam etmeden önce senkronizasyonun bittiğine emin olun.
+upgrade.version3dropBundleExtension.err.alreadyExists = Otomatik format değiştirme sırasında hata. "%s" zaten bulunmakta.
+# unlock.fxml
+unlock.label.password = Şifre
+unlock.label.mountName = Sürücü ismi
+unlock.label.winDriveLetter = Sürücü konumu
+unlock.label.downloadsPageLink = Tüm Cryptomator sürümleri
+unlock.label.advancedHeading = Gelişmiş seçenekler
+unlock.button.unlock = Kasayı aç
+unlock.button.advancedOptions.show = Daha fazla seçenek
+unlock.button.advancedOptions.hide = Daha az seçenek
+unlock.choicebox.winDriveLetter.auto = Otomatik ata
+unlock.errorMessage.wrongPassword = Yanlış şifre
+unlock.errorMessage.mountingFailed = Başlama başarısız. Detaylar için log dosyasına bakın
+unlock.errorMessage.unsupportedKeyLengthInstallJCE = Şifre çözme başarısız. Lütfen Oracle JCE Unlimited Strength Policy yükleyin.
+unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware = Desteklenmeyen kasa. Bu kasa daha eski bir Cryptomator sürümü ile oluşturulmuş.
+unlock.errorMessage.unsupportedVersion.softwareOlderThanVault = Desteklenmeyen kasa. Bu kasa daha yeni bir Cryptomator sürümü ile oluşturulmuş.
+unlock.messageLabel.startServerFailed = WebDAV sunucu başlatma başarısız.
+# change_password.fxml
+changePassword.label.oldPassword = Eski şifre
+changePassword.label.newPassword = Yeni şifre
+changePassword.label.retypePassword = Yeni şifre (tekrar)
+changePassword.label.downloadsPageLink = Tüm Cryptomator sürümleri
+changePassword.button.change = Şifreyi değiştir
+changePassword.errorMessage.wrongPassword = Yanlış şifre
+changePassword.errorMessage.decryptionFailed = Şifre çözme başarısız
+changePassword.errorMessage.unsupportedKeyLengthInstallJCE = Şifre çözme başarısız. Lütfen Oracle JCE Unlimited Strength Policy yükleyin.
+changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware = Desteklenmeyen kasa. Bu kasa daha eski bir Cryptomator sürümü ile oluşturulmuş.
+changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault = Desteklenmeyen kasa. Bu kasa daha yeni bir Cryptomator sürümü ile oluşturulmuş.
+changePassword.infoMessage.success = Şifre değiştirildi.
+# unlocked.fxml
+unlocked.button.lock = Kasayı kilitle
+unlocked.moreOptions.reveal = Sürücüyü göster
+unlocked.moreOptions.copyUrl = WebDAV URl'sini kopyala
+unlocked.label.revealFailed = Komut başarısız
+unlocked.label.unmountFailed = Sürücü çıkarma başarısız
+unlocked.label.statsEncrypted = şifrelenmiş
+unlocked.label.statsDecrypted = şifresi çözülmüş
+unlocked.ioGraph.yAxis.label = Veri hacmi (MiB/s)
+# mac_warnings.fxml
+macWarnings.windowTitle = Tehlike - Bozuk dosya - %s
+macWarnings.message = Cryptomator bu satırlarda kötücül olma olasılığına sahip bozulmalar tespit etti\:
+macWarnings.moreInformationButton = Daha fazla öğren
+macWarnings.whitelistButton = Seçilenleri her halükarda şifrele
+# settings.fxml
+settings.version.label = Sürüm %s
+settings.checkForUpdates.label = Güncellemeleri denetle
+settings.port.label = WebDAV Port *
+settings.port.prompt = 0 \= Otomatik seç
+settings.useipv6.label = IPv6 kullan
+settings.requiresRestartLabel = * Cryptomator yeniden başlatılması gerek
+# tray icon
+tray.menu.open = Aç
+tray.menu.quit = Çıkış
+tray.infoMsg.title = Hala çalışıyor
+tray.infoMsg.msg = Cryptomator hala çalışıyor. Bildirim simgesi ile çıkış yapın.
+tray.infoMsg.msg.osx = Cryptomator hala çalışıyor. Menü bar simgesi ile çıkış yapın.
+initialize.messageLabel.passwordStrength.0 = Çok zayıf
+initialize.messageLabel.passwordStrength.1 = Zayıf
+initialize.messageLabel.passwordStrength.2 = İyi
+initialize.messageLabel.passwordStrength.3 = Güçlü
+initialize.messageLabel.passwordStrength.4 = Çok güçlü
\ No newline at end of file