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 index 4f9f9f7c7..11bc14f77 100644 --- 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 @@ -12,6 +12,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; +import org.cryptomator.io.FileContents; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +53,13 @@ final class ConflictResolver { Folder folder = conflictingFile.parent().get(); File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext); if (canonicalFile.exists()) { - // conflict detected! look for an alternative name: + // there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict: + if (isDirectory && FileContents.UTF_8.readContents(canonicalFile).equals(FileContents.UTF_8.readContents(conflictingFile))) { + conflictingFile.delete(); + return canonicalFile; + } + + // conventional conflict detected! look for an alternative name: File alternativeFile; String conflictId; do { 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 f83b1087f..363113a39 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 @@ -89,7 +89,7 @@ class CryptoFolder extends CryptoNode implements Folder { private Stream nonConflictingFiles() { final Stream files = physicalFolder().filter(Folder::exists).map(Folder::files).orElse(Stream.empty()); - return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary); + return files.filter(containsEncryptedName()).map(conflictResolver::resolveIfNecessary).distinct(); } private Predicate containsEncryptedName() { 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 index 7276d9b15..b99a000b4 100644 --- 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 @@ -1,5 +1,6 @@ package org.cryptomator.filesystem.crypto; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.function.Function; @@ -9,10 +10,13 @@ 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.cryptomator.filesystem.ReadableFile; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; public class ConflictResolverTest { @@ -101,10 +105,54 @@ public class ConflictResolverTest { } @Test - public void testConflictingFolder() { - File resolved = conflictResolver.resolveIfNecessary(conflictingFolder); + public void testConflictingFolderWithDifferentId() { + ReadableFile directoryId1 = Mockito.mock(ReadableFile.class); + ReadableFile directoryId2 = Mockito.mock(ReadableFile.class); + Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1); + Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId2); + Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1")); + Mockito.when(directoryId2.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id2")); + + File result = conflictResolver.resolveIfNecessary(conflictingFolder); Mockito.verify(conflictingFolder).moveTo(resolved); - Assert.assertSame(resolved, resolved); + Assert.assertSame(resolved, result); + } + + @Test + public void testConflictingFolderWithSameId() { + ReadableFile directoryId1 = Mockito.mock(ReadableFile.class); + Mockito.when(canonicalFolder.openReadable()).thenReturn(directoryId1); + Mockito.when(conflictingFolder.openReadable()).thenReturn(directoryId1); + Mockito.when(directoryId1.read(Mockito.any())).thenAnswer(new FillBufferAnswer("id1")); + + File result = conflictResolver.resolveIfNecessary(conflictingFolder); + Mockito.verify(conflictingFolder).delete(); + Assert.assertSame(canonicalFolder, result); + } + + private static class FillBufferAnswer implements Answer { + + private final byte[] content; + private int bytesRead = 0; + + public FillBufferAnswer(String content) { + this.content = content.getBytes(StandardCharsets.UTF_8); + } + + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + if (bytesRead >= content.length) { + bytesRead = 0; + return -1; + } else { + ByteBuffer buf = invocation.getArgumentAt(0, ByteBuffer.class); + int delta = Math.min(content.length - bytesRead, buf.remaining()); + buf.put(content, bytesRead, delta); + bytesRead += delta; + return content.length; + } + } + } }