diff --git a/main/filesystem-charsets/pom.xml b/main/filesystem-charsets/pom.xml
new file mode 100644
index 000000000..b5eba0555
--- /dev/null
+++ b/main/filesystem-charsets/pom.xml
@@ -0,0 +1,45 @@
+
+
+
+ 4.0.0
+
+ org.cryptomator
+ main
+ 1.1.0-SNAPSHOT
+
+ filesystem-charsets
+ Cryptomator filesystem: Filename charset compatibility layer
+
+
+
+ org.cryptomator
+ filesystem-api
+
+
+
+
+ org.cryptomator
+ commons-test
+
+
+ org.cryptomator
+ filesystem-inmemory
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java
new file mode 100644
index 000000000..af73d59c1
--- /dev/null
+++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFile.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.io.UncheckedIOException;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.delegating.DelegatingFile;
+
+class NormalizedNameFile extends DelegatingFile {
+
+ private final Form displayForm;
+
+ public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) {
+ super(parent, delegate);
+ this.displayForm = displayForm;
+ }
+
+ @Override
+ public String name() throws UncheckedIOException {
+ return Normalizer.normalize(super.name(), displayForm);
+ }
+
+}
diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java
new file mode 100644
index 000000000..a69c572d6
--- /dev/null
+++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystem.java
@@ -0,0 +1,27 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.text.Normalizer.Form;
+
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
+
+public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem {
+
+ public NormalizedNameFileSystem(Folder delegate, Form displayForm) {
+ super(null, delegate, displayForm);
+ }
+
+ @Override
+ public Folder getDelegate() {
+ return delegate;
+ }
+
+}
diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java
new file mode 100644
index 000000000..e2762059b
--- /dev/null
+++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/NormalizedNameFolder.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.io.UncheckedIOException;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.delegating.DelegatingFolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class NormalizedNameFolder extends DelegatingFolder {
+
+ private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class);
+ private final Form displayForm;
+
+ public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) {
+ super(parent, delegate);
+ this.displayForm = displayForm;
+ }
+
+ @Override
+ public String name() throws UncheckedIOException {
+ return Normalizer.normalize(super.name(), displayForm);
+ }
+
+ @Override
+ public NormalizedNameFile file(String name) throws UncheckedIOException {
+ String nfcName = Normalizer.normalize(name, Form.NFC);
+ String nfdName = Normalizer.normalize(name, Form.NFD);
+ NormalizedNameFile nfcFile = super.file(nfcName);
+ NormalizedNameFile nfdFile = super.file(nfdName);
+ if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
+ LOG.warn("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
+ } else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
+ LOG.info("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
+ nfdFile.moveTo(nfcFile);
+ }
+ return nfcFile;
+ }
+
+ @Override
+ protected NormalizedNameFile newFile(File delegate) {
+ return new NormalizedNameFile(this, delegate, displayForm);
+ }
+
+ @Override
+ public NormalizedNameFolder folder(String name) throws UncheckedIOException {
+ String nfcName = Normalizer.normalize(name, Form.NFC);
+ String nfdName = Normalizer.normalize(name, Form.NFD);
+ NormalizedNameFolder nfcFolder = super.folder(nfcName);
+ NormalizedNameFolder nfdFolder = super.folder(nfdName);
+ if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
+ LOG.warn("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
+ } else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
+ LOG.info("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
+ nfdFolder.moveTo(nfcFolder);
+ }
+ return nfcFolder;
+ }
+
+ @Override
+ protected NormalizedNameFolder newFolder(Folder delegate) {
+ return new NormalizedNameFolder(this, delegate, displayForm);
+ }
+
+}
diff --git a/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java
new file mode 100644
index 000000000..3ee4f6a17
--- /dev/null
+++ b/main/filesystem-charsets/src/main/java/org/cryptomator/filesystem/charsets/package-info.java
@@ -0,0 +1,16 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+/**
+ * Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C.
+ * Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form.
+ * It is recommended to use NFD for OS X and NFC for other operating systems.
+ * When looking for a file or folder with a name given in either form, both possibilities are considered
+ * and files/folders stored in NFD are automatically migrated to NFC.
+ */
+package org.cryptomator.filesystem.charsets;
\ No newline at end of file
diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java
new file mode 100644
index 000000000..e662838a8
--- /dev/null
+++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileSystemTest.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.nio.ByteBuffer;
+import java.text.Normalizer.Form;
+
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.WritableFile;
+import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class NormalizedNameFileSystemTest {
+
+ @Test
+ public void testFileMigration() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) {
+ writable.write(ByteBuffer.allocate(0));
+ }
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
+ Assert.assertTrue(normalizationFs.file("\u00F4").exists());
+ Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
+ Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
+ Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
+ }
+
+ @Test
+ public void testNoFileMigration() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) {
+ writable.write(ByteBuffer.allocate(0));
+ }
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
+ Assert.assertTrue(normalizationFs.file("\u00F4").exists());
+ Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
+ Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
+ Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
+ }
+
+ @Test
+ public void testFolderMigration() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ inMemoryFs.folder("\u006F\u0302").create();
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
+ Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
+ Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
+ Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
+ Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
+ }
+
+ @Test
+ public void testNoFolderMigration() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ inMemoryFs.folder("\u00F4").create();
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
+ Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
+ Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
+ Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
+ Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
+ }
+
+ @Test
+ public void testNfcDisplayNames() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ inMemoryFs.folder("a\u00F4").create();
+ inMemoryFs.folder("b\u006F\u0302").create();
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
+ Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name());
+ Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name());
+ }
+
+ @Test
+ public void testNfdDisplayNames() {
+ FileSystem inMemoryFs = new InMemoryFileSystem();
+ inMemoryFs.folder("a\u00F4").create();
+ inMemoryFs.folder("b\u006F\u0302").create();
+ FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD);
+ Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name());
+ Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name());
+ }
+
+}
diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java
new file mode 100644
index 000000000..3a3f1fe64
--- /dev/null
+++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFileTest.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.text.Normalizer.Form;
+
+import org.cryptomator.filesystem.File;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class NormalizedNameFileTest {
+
+ private File delegateNfc;
+ private File delegateNfd;
+
+ @Before
+ public void setup() {
+ delegateNfc = Mockito.mock(File.class);
+ delegateNfd = Mockito.mock(File.class);
+ Mockito.when(delegateNfc.name()).thenReturn("\u00C5");
+ Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A");
+ }
+
+ @Test
+ public void testDisplayNameInNfc() {
+ File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC);
+ File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC);
+ Assert.assertEquals("\u00C5", file1.name());
+ Assert.assertEquals("\u00C5", file2.name());
+ }
+
+ @Test
+ public void testDisplayNameInNfd() {
+ File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD);
+ File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD);
+ Assert.assertEquals("\u0041\u030A", file1.name());
+ Assert.assertEquals("\u0041\u030A", file2.name());
+ }
+
+}
diff --git a/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java
new file mode 100644
index 000000000..7bf017652
--- /dev/null
+++ b/main/filesystem-charsets/src/test/java/org/cryptomator/filesystem/charsets/NormalizedNameFolderTest.java
@@ -0,0 +1,149 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.filesystem.charsets;
+
+import java.text.Normalizer.Form;
+
+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 NormalizedNameFolderTest {
+
+ private Folder delegate;
+ private File delegateSubFileNfc;
+ private File delegateSubFileNfd;
+ private Folder delegateSubFolderNfc;
+ private Folder delegateSubFolderNfd;
+
+ @Before
+ public void setup() {
+ delegate = Mockito.mock(Folder.class);
+ delegateSubFileNfc = Mockito.mock(File.class);
+ delegateSubFileNfd = Mockito.mock(File.class);
+ Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc);
+ Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5");
+ Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd);
+ Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A");
+ delegateSubFolderNfc = Mockito.mock(Folder.class);
+ delegateSubFolderNfd = Mockito.mock(Folder.class);
+ Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc);
+ Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4");
+ Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd);
+ Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302");
+ }
+
+ @Test
+ public void testDisplayNameInNfc() {
+ Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC);
+ Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC);
+ Assert.assertEquals("\u00F4", folder1.name());
+ Assert.assertEquals("\u00F4", folder2.name());
+ }
+
+ @Test
+ public void testDisplayNameInNfd() {
+ Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD);
+ Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD);
+ Assert.assertEquals("\u006F\u0302", folder1.name());
+ Assert.assertEquals("\u006F\u0302", folder2.name());
+ }
+
+ @Test
+ public void testNoFolderMigration1() {
+ Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
+ Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ Folder sub1 = folder.folder("\u00F4");
+ Folder sub2 = folder.folder("\u006F\u0302");
+ Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testNoFolderMigration2() {
+ Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
+ Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ Folder sub1 = folder.folder("\u00F4");
+ Folder sub2 = folder.folder("\u006F\u0302");
+ Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testNoFolderMigration3() {
+ Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
+ Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ Folder sub1 = folder.folder("\u00F4");
+ Folder sub2 = folder.folder("\u006F\u0302");
+ Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testFolderMigration() {
+ Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
+ Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ Folder sub1 = folder.folder("\u00F4");
+ Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc);
+ Folder sub2 = folder.folder("\u006F\u0302");
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testNoFileMigration1() {
+ Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
+ Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ File sub1 = folder.file("\u00C5");
+ File sub2 = folder.file("\u0041\u030A");
+ Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testNoFileMigration2() {
+ Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
+ Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ File sub1 = folder.file("\u00C5");
+ File sub2 = folder.file("\u0041\u030A");
+ Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testNoFileMigration3() {
+ Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
+ Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ File sub1 = folder.file("\u00C5");
+ File sub2 = folder.file("\u0041\u030A");
+ Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
+ Assert.assertSame(sub1, sub2);
+ }
+
+ @Test
+ public void testFileMigration() {
+ Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
+ Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
+ Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
+ File sub1 = folder.file("\u00C5");
+ Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc);
+ File sub2 = folder.file("\u0041\u030A");
+ Assert.assertSame(sub1, sub2);
+ }
+
+}
diff --git a/main/filesystem-charsets/src/test/resources/log4j2.xml b/main/filesystem-charsets/src/test/resources/log4j2.xml
new file mode 100644
index 000000000..9b4889392
--- /dev/null
+++ b/main/filesystem-charsets/src/test/resources/log4j2.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/filesystem-invariants-tests/pom.xml b/main/filesystem-invariants-tests/pom.xml
index aa13917ee..054464338 100644
--- a/main/filesystem-invariants-tests/pom.xml
+++ b/main/filesystem-invariants-tests/pom.xml
@@ -20,6 +20,10 @@
org.cryptomator
filesystem-api
+
+ org.cryptomator
+ filesystem-charsets
+
org.cryptomator
filesystem-crypto
diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java
index 5f9a423f5..8f1eed8ca 100644
--- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java
+++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java
@@ -4,11 +4,13 @@ import static org.cryptomator.common.test.TempFilesRemovedOnShutdown.createTempD
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.text.Normalizer.Form;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoEngineTestModule;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemTestComponent;
@@ -35,8 +37,10 @@ class FileSystemFactories implements Iterable {
add("ShorteningFileSystem > InMemoryFileSystem", this::createShorteningFileSystemInMemory);
add("StatsFileSystem > NioFileSystem", this::createStatsFileSystemNio);
add("StatsFileSystem > InMemoryFileSystem", this::createStatsFileSystemInMemory);
- add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
- add("StatsFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
+ add("NormalizingFileSystem > NioFileSystem", this::createNormalizingFileSystemNio);
+ add("NormalizingFileSystem > InMemoryFileSystem", this::createNormalizingFileSystemInMemory);
+ add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > InMemoryFileSystem", this::createCompoundFileSystemInMemory);
+ add("StatsFileSystem > NormalizingFileSystem > CryptoFileSystem > ShorteningFileSystem > NioFileSystem", this::createCompoundFileSystemNio);
}
private FileSystem createCryptoFileSystemInMemory() {
@@ -63,6 +67,14 @@ class FileSystemFactories implements Iterable {
return createStatsFileSystem(createInMemoryFileSystem());
}
+ private FileSystem createNormalizingFileSystemNio() {
+ return createNormalizingFileSystem(createInMemoryFileSystem());
+ }
+
+ private FileSystem createNormalizingFileSystemInMemory() {
+ return createNormalizingFileSystem(createInMemoryFileSystem());
+ }
+
private FileSystem createCompoundFileSystemNio() {
return createCompoundFileSystem(createNioFileSystem());
}
@@ -84,13 +96,17 @@ class FileSystemFactories implements Iterable {
}
private FileSystem createCompoundFileSystem(FileSystem delegate) {
- return createStatsFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate)));
+ return createStatsFileSystem(createNormalizingFileSystem(createCryptoFileSystem(createShorteningFileSystem(delegate))));
}
private FileSystem createStatsFileSystem(FileSystem delegate) {
return new StatsFileSystem(delegate);
}
+ private FileSystem createNormalizingFileSystem(FileSystem delegate) {
+ return new NormalizedNameFileSystem(delegate, Form.NFC);
+ }
+
private FileSystem createCryptoFileSystem(FileSystem delegate) {
CRYPTO_FS_COMP.cryptoFileSystemFactory().initializeNew(delegate, "aPassphrase");
return CRYPTO_FS_COMP.cryptoFileSystemFactory().unlockExisting(delegate, "aPassphrase", Mockito.mock(CryptoFileSystemDelegate.class));
diff --git a/main/pom.xml b/main/pom.xml
index 1aac82079..75ba59663 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -80,6 +80,11 @@
filesystem-api
${project.version}
+
+ org.cryptomator
+ filesystem-charsets
+ ${project.version}
+
org.cryptomator
filesystem-nio
@@ -286,6 +291,7 @@
frontend-api
frontend-webdav
ui
+ filesystem-charsets
diff --git a/main/ui/pom.xml b/main/ui/pom.xml
index 6808f3887..873c1af38 100644
--- a/main/ui/pom.xml
+++ b/main/ui/pom.xml
@@ -38,6 +38,10 @@
org.cryptomator
filesystem-crypto
+
+ org.cryptomator
+ filesystem-charsets
+
org.cryptomator
filesystem-stats
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
index 376e56d09..3cbc4bbc1 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
@@ -30,6 +30,7 @@ import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.nio.NioFileSystem;
@@ -126,7 +127,8 @@ public class Vault implements CryptoFileSystemDelegate {
FileSystem fs = getNioFileSystem();
FileSystem shorteningFs = shorteningFileSystemFactory.get(fs);
FileSystem cryptoFs = cryptoFileSystemFactory.unlockExisting(shorteningFs, passphrase, this);
- StatsFileSystem statsFs = new StatsFileSystem(cryptoFs);
+ FileSystem normalizingFs = new NormalizedNameFileSystem(cryptoFs, SystemUtils.IS_OS_MAC_OSX ? Form.NFD : Form.NFC);
+ StatsFileSystem statsFs = new StatsFileSystem(normalizingFs);
statsFileSystem = Optional.of(statsFs);
String contextPath = StringUtils.prependIfMissing(mountName, "/");
Frontend frontend = frontendFactory.create(statsFs, contextPath);