Sebastian Stenzel 9 年之前
父節點
當前提交
b77d4b5ae2

+ 45 - 0
main/filesystem-charsets/pom.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright (c) 2015 Sebastian Stenzel
+  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
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.cryptomator</groupId>
+		<artifactId>main</artifactId>
+		<version>1.0.4</version>
+	</parent>
+	<artifactId>filesystem-charsets</artifactId>
+	<name>Cryptomator filesystem: Filename charset compatibility layer</name>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-api</artifactId>
+		</dependency>
+
+		<!-- Tests -->
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>commons-test</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-inmemory</artifactId>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+</project>

+ 32 - 0
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<NormalizedNameFolder> {
+
+	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);
+	}
+
+}

+ 27 - 0
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;
+	}
+
+}

+ 76 - 0
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<NormalizedNameFolder, NormalizedNameFile> {
+
+	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);
+	}
+
+}

+ 16 - 0
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;

+ 90 - 0
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());
+	}
+
+}

+ 48 - 0
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());
+	}
+
+}

+ 149 - 0
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);
+	}
+
+}

+ 22 - 0
main/filesystem-charsets/src/test/resources/log4j2.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Configuration status="WARN">
+
+	<Appenders>
+		<Console name="Console" target="SYSTEM_OUT">
+			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
+			<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
+		</Console>
+		<Console name="StdErr" target="SYSTEM_ERR">
+			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
+			<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
+		</Console>
+	</Appenders>
+
+	<Loggers>
+		<Root level="DEBUG">
+			<AppenderRef ref="Console" />
+			<AppenderRef ref="StdErr" />
+		</Root>
+	</Loggers>
+
+</Configuration>

+ 4 - 0
main/filesystem-invariants-tests/pom.xml

@@ -20,6 +20,10 @@
 			<groupId>org.cryptomator</groupId>
 			<artifactId>filesystem-api</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-charsets</artifactId>
+		</dependency>
 		<dependency>
 			<groupId>org.cryptomator</groupId>
 			<artifactId>filesystem-crypto</artifactId>

+ 19 - 3
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<FileSystemFactory> {
 		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<FileSystemFactory> {
 		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<FileSystemFactory> {
 	}
 
 	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));

+ 6 - 0
main/pom.xml

@@ -80,6 +80,11 @@
 				<artifactId>filesystem-api</artifactId>
 				<version>${project.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>org.cryptomator</groupId>
+				<artifactId>filesystem-charsets</artifactId>
+				<version>${project.version}</version>
+			</dependency>
 			<dependency>
 				<groupId>org.cryptomator</groupId>
 				<artifactId>filesystem-nio</artifactId>
@@ -294,6 +299,7 @@
 		<module>frontend-api</module>
 		<module>frontend-webdav</module>
 		<module>ui</module>
+		<module>filesystem-charsets</module>
 	</modules>
 
 	<profiles>

+ 4 - 0
main/ui/pom.xml

@@ -38,6 +38,10 @@
 			<groupId>org.cryptomator</groupId>
 			<artifactId>filesystem-crypto</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.cryptomator</groupId>
+			<artifactId>filesystem-charsets</artifactId>
+		</dependency>
 		<dependency>
 			<groupId>org.cryptomator</groupId>
 			<artifactId>filesystem-stats</artifactId>

+ 3 - 1
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);