diff --git a/pom.xml b/pom.xml
index 2b42ecab..dccce7ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,7 @@
5.11.3
+ 1.37
5.14.2
3.0
1.3.0
@@ -123,6 +124,18 @@
${junit.jupiter.version}
test
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+ test
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ provided
+
org.mockito
mockito-core
@@ -163,6 +176,11 @@
dagger-compiler
${dagger.version}
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
index 4a7db946..9ca8aacd 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
@@ -61,7 +61,7 @@ public Stream process(Node node) {
Path canonicalPath = node.ciphertextPath.resolveSibling(canonicalCiphertextFileName);
return resolveConflict(node, canonicalPath);
} catch (IOException e) {
- LOG.error("Failed to resolve conflict for " + node.ciphertextPath, e);
+ LOG.error("Failed to resolve conflict for {}", node.ciphertextPath, e);
return Stream.empty();
}
}
@@ -75,7 +75,7 @@ private Stream resolveConflict(Node conflicting, Path canonicalPath) throw
resolved.extractedCiphertext = conflicting.extractedCiphertext;
return Stream.of(resolved);
} else {
- return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, conflicting.cleartextName));
+ return renameConflictingFile(canonicalPath, conflicting);
}
}
@@ -83,35 +83,56 @@ private Stream resolveConflict(Node conflicting, Path canonicalPath) throw
* Resolves a conflict by renaming the conflicting file.
*
* @param canonicalPath The path to the original (conflict-free) file.
- * @param conflictingPath The path to the potentially conflicting file.
- * @param cleartext The cleartext name of the conflicting file.
- * @return The newly created Node after renaming the conflicting file.
- * @throws IOException
+ * @param conflicting The conflicting file.
+ * @return The newly created Node if rename succeeded or an empty stream otherwise.
+ * @throws IOException If an unexpected I/O exception occurs during rename
*/
- private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException {
+ private Stream renameConflictingFile(Path canonicalPath, Node conflicting) throws IOException {
assert Files.exists(canonicalPath);
- final int beginOfFileExtension = cleartext.lastIndexOf('.');
- final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : "";
- final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext;
- final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextFileNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
- String alternativeCleartext;
- String alternativeCiphertext;
- String alternativeCiphertextName;
- Path alternativePath;
- int i = 1;
- do {
- alternativeCleartext = lengthRestrictedBasename + " (" + i++ + ")" + fileExtension;
+ assert conflicting.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX);
+ assert conflicting.fullCiphertextFileName.contains(conflicting.extractedCiphertext);
+
+ final String cleartext = conflicting.cleartextName;
+ final int beginOfCleartextExt = cleartext.lastIndexOf('.');
+ final String cleartextFileExt = (beginOfCleartextExt > 0) ? cleartext.substring(beginOfCleartextExt) : "";
+ final String cleartextBasename = (beginOfCleartextExt > 0) ? cleartext.substring(0, beginOfCleartextExt) : cleartext;
+
+ // let's assume that some the sync conflict string is added at the end of the file name, but before .c9r:
+ final int endOfCiphertext = conflicting.fullCiphertextFileName.indexOf(conflicting.extractedCiphertext) + conflicting.extractedCiphertext.length();
+ final String originalConflictSuffix = conflicting.fullCiphertextFileName.substring(endOfCiphertext, conflicting.fullCiphertextFileName.length() - Constants.CRYPTOMATOR_FILE_SUFFIX.length());
+
+ // split available maxCleartextFileNameLength between basename, conflict suffix, and file extension:
+ final int netCleartext = maxCleartextFileNameLength - cleartextFileExt.length(); // file extension must be preserved
+ final String conflictSuffix = originalConflictSuffix.substring(0, Math.min(originalConflictSuffix.length(), netCleartext / 2)); // max 50% of available space
+ final int conflictSuffixLen = Math.max(4, conflictSuffix.length()); // prefer to use original conflict suffix, but reserver at least 4 chars for numerical fallback: " (9)"
+ final String lengthRestrictedBasename = cleartextBasename.substring(0, Math.min(cleartextBasename.length(), netCleartext - conflictSuffixLen)); // remaining space for basename
+
+ // attempt to use original conflict suffix:
+ String alternativeCleartext = lengthRestrictedBasename + conflictSuffix + cleartextFileExt;
+ String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId);
+ String alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
+ Path alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
+
+ // fallback to number conflic suffix, if file with alternative path already exists:
+ for (int i = 1; i < 10 && Files.exists(alternativePath); i++) {
+ alternativeCleartext = lengthRestrictedBasename + " (" + i + ")" + cleartextFileExt;
alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId);
alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
- } while (Files.exists(alternativePath));
+ }
+
assert alternativeCiphertextName.length() <= maxC9rFileNameLength;
- LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
- Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
+ if (Files.exists(alternativePath)) {
+ LOG.warn("Failed finding alternative name for {}. Keeping original name.", conflicting.ciphertextPath);
+ return Stream.empty();
+ }
+
+ Files.move(conflicting.ciphertextPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
+ LOG.info("Renamed conflicting file {} to {}...", conflicting.ciphertextPath, alternativePath);
Node node = new Node(alternativePath);
node.cleartextName = alternativeCleartext;
node.extractedCiphertext = alternativeCiphertext;
- return node;
+ return Stream.of(node);
}
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java
index e3bf1fda..1347f9d0 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java
@@ -20,7 +20,7 @@
class C9rDecryptor {
// visible for testing:
- static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}");
+ static final Pattern BASE64_PATTERN = Pattern.compile("[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*(?:[a-zA-Z0-9-_]{4}|[a-zA-Z0-9-_]{3}=|[a-zA-Z0-9-_]{2}==)");
private static final CharMatcher DELIM_MATCHER = CharMatcher.anyOf("_-");
private final Cryptor cryptor;
@@ -36,11 +36,7 @@ public Stream process(Node node) {
String basename = StringUtils.removeEnd(node.fullCiphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX);
Matcher matcher = BASE64_PATTERN.matcher(basename);
Optional match = extractCiphertext(node, matcher, 0, basename.length());
- if (match.isPresent()) {
- return Stream.of(match.get());
- } else {
- return Stream.empty();
- }
+ return match.stream();
}
private Optional extractCiphertext(Node node, Matcher matcher, int start, int end) {
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/Base64UrlRegexTest.java b/src/test/java/org/cryptomator/cryptofs/dir/Base64UrlRegexTest.java
new file mode 100644
index 00000000..209369a3
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/dir/Base64UrlRegexTest.java
@@ -0,0 +1,136 @@
+package org.cryptomator.cryptofs.dir;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
+ */
+public class Base64UrlRegexTest {
+
+ private static final String[] TEST_INPUTS = { //
+ "aaaaBBBBccccDDDDeeeeFFFF",
+ "aaaaBBBBccccDDDDeeeeFFF=",
+ "aaaaBBBBccccDDDDeeeeFF==",
+ "aaaaBBBBcc0123456789_-==",
+ "aaaaBBBBccccDDDDeeeeFFFFggggHH==",
+ "-3h6-FSsYhMCJHSAV9cjJ89F7R73b0zIB4vvO01b7uWq28fWioRagWpMv-w0MA-2ORjbShuv", //
+ "iJYw7QpVjKTDv_b6H5jLkauVrnPcGbV9DnIG426EBzjlYmRuJDX5cjFJLmTDA7EOEmo5rAHT3Jc=", //
+ "PnBpirtqhCKm9hE1341rxkqdASfyYJqNHROxR1xJWDH6TGbeqqXn_sr2Rk5zzVpSbufkiqZH9a==", //
+ "S8cmirjkHBHbIJZXExbFyyTOA8r6TvTPddK_sdQZpcE3RCMDI0mo9w2f53DSkqT0xRf1xcrmxvU=" //
+ };
+
+ private static final String[] TEST_INVALID_INPUTS = { //
+ "aaaaBBBBccccDDDDeeee", // too short
+ "aaaaBBBBccccDDDDeeeeFFF", // unpadded
+ "====BBBBccccDDDDeeeeFFFF", // padding not at end
+ "????BBBBccccDDDDeeeeFFFF", // invalid chars
+ "conflict aaaaBBBBccccDDDDeeeeFFFF", // only a partial match
+ "aaaaBBBBccccDDDDeeeeFFFF conflict", // only a partial match
+ "-3h6-FSsYhMCJHSAV9cjJ89F7R73b0zIB4vvO01b7uWq28fWioRagWpMv-w0MA-2ORjbShu", // not multiple of four
+ "=iJYw7QpVjKTDv_b6H5jLkauVrnPcGbV9DnIG426EBzjlYmRuJDX5cjFJLmTDA7EOEmo5rAHT3J=", // padding in wrong position
+ "PnBp.irtqhCKm9hE1341rxkqdASfyYJqNHROxR1xJWDH6TGbeqqXn_sr2Rk5zzVpSbufkiqZH9a==", // invalid character
+ };
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*(?:[a-zA-Z0-9-_]{4}|[a-zA-Z0-9-_]{3}=|[a-zA-Z0-9-_]{2}==)", // most strict
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
+ })
+ public void testValidBase64Pattern(String patternString) {
+ Pattern pattern = Pattern.compile(patternString);
+ for (String input : TEST_INPUTS) {
+ Matcher matcher = pattern.matcher(input);
+ Assertions.assertTrue(matcher.matches(), "pattern does not match " + input);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*(?:[a-zA-Z0-9-_]{4}|[a-zA-Z0-9-_]{3}=|[a-zA-Z0-9-_]{2}==)", // most strict
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
+ })
+ public void testInvalidInputs(String patternString) {
+ Pattern pattern = Pattern.compile(patternString);
+ for (String input : TEST_INVALID_INPUTS) {
+ Matcher matcher = pattern.matcher(input);
+ Assertions.assertFalse(matcher.matches(), "pattern matches " + input);
+ }
+ }
+
+ @Test
+ @Disabled // only run manually
+ public void runBenchmarks() throws RunnerException {
+ // Taken from http://stackoverflow.com/a/30486197/4014509:
+ Options opt = new OptionsBuilder().include(RegexBenchmark.class.getSimpleName()).build();
+ new Runner(opt).run();
+ }
+
+ @State(Scope.Thread)
+ @Fork(3)
+ @Warmup(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+ @Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+ @BenchmarkMode(value = {Mode.AverageTime})
+ @OutputTimeUnit(TimeUnit.MICROSECONDS)
+ public static class RegexBenchmark {
+
+ // Base64 regex pattern
+ @Param({
+ "([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*(?:[a-zA-Z0-9-_]{4}|[a-zA-Z0-9-_]{3}=|[a-zA-Z0-9-_]{2}==)", // most strict
+ "[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
+ })
+ private String patternString;
+
+ private Pattern pattern;
+
+ @Setup(Level.Trial)
+ public void compilePattern() {
+ pattern = Pattern.compile(patternString);
+ }
+
+ @Benchmark
+ public void testPattern(Blackhole bh) {
+ for (String input : TEST_INPUTS) {
+ Matcher matcher = pattern.matcher(input);
+ bh.consume(matcher.matches());
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
index fc868950..d7a7ba00 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
@@ -8,7 +8,6 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
@@ -31,7 +30,7 @@ public void setup() {
fileNameCryptor = Mockito.mock(FileNameCryptor.class);
vaultConfig = Mockito.mock(VaultConfig.class);
Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
- Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(44); // results in max cleartext size = 14
+ Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(84); // results in max cleartext size = 44
conflictResolver = new C9rConflictResolver(cryptor, "foo", vaultConfig);
}
@@ -60,10 +59,10 @@ public void testResolveHiddenNode(String filename) {
@Test
public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throws IOException {
- Files.createFile(dir.resolve("foo (1).c9r"));
+ Files.createFile(dir.resolve("foo (Created by Alice).c9r"));
Files.createFile(dir.resolve("foo.c9r"));
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
- Node unresolved = new Node(dir.resolve("foo (1).c9r"));
+ Node unresolved = new Node(dir.resolve("foo (Created by Alice).c9r"));
unresolved.cleartextName = "bar.txt";
unresolved.extractedCiphertext = "foo";
@@ -72,6 +71,26 @@ public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throw
Assertions.assertNotEquals(unresolved, resolved);
Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName);
+ Assertions.assertEquals("bar (Created by Alice).txt", resolved.cleartextName);
+ Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
+ Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
+ }
+
+ @Test
+ public void testResolveConflictingFileByAddingNumericSuffix(@TempDir Path dir) throws IOException {
+ Files.createFile(dir.resolve("foo (Created by Alice).c9r"));
+ Files.createFile(dir.resolve("foo.c9r"));
+ Files.createFile(dir.resolve("baz.c9r")); // resolved name already occupied, try cux next!
+ Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz").thenReturn("qux");
+ Node unresolved = new Node(dir.resolve("foo (Created by Alice).c9r"));
+ unresolved.cleartextName = "bar.txt";
+ unresolved.extractedCiphertext = "foo";
+
+ Stream result = conflictResolver.process(unresolved);
+ Node resolved = result.findAny().get();
+
+ Assertions.assertNotEquals(unresolved, resolved);
+ Assertions.assertEquals("qux.c9r", resolved.fullCiphertextFileName);
Assertions.assertEquals("bar (1).txt", resolved.cleartextName);
Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
@@ -79,11 +98,11 @@ public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throw
@Test
public void testResolveConflictingFileByChoosingNewLengthLimitedName(@TempDir Path dir) throws IOException {
- Files.createFile(dir.resolve("foo (1).c9r"));
+ Files.createFile(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
Files.createFile(dir.resolve("foo.c9r"));
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
- Node unresolved = new Node(dir.resolve("foo (1).c9r"));
- unresolved.cleartextName = "hello world.txt";
+ Node unresolved = new Node(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
+ unresolved.cleartextName = "this is a rather long file name.txt";
unresolved.extractedCiphertext = "foo";
Stream result = conflictResolver.process(unresolved);
@@ -91,11 +110,27 @@ public void testResolveConflictingFileByChoosingNewLengthLimitedName(@TempDir Pa
Assertions.assertNotEquals(unresolved, resolved);
Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName);
- Assertions.assertEquals("hello (1).txt", resolved.cleartextName);
+ Assertions.assertEquals("this is a rather lon (Created by Alice o.txt", resolved.cleartextName);
Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
}
+ @Test
+ public void testResolveConflictFailedAlternativeNamesReserved(@TempDir Path dir) throws IOException {
+ Files.createFile(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
+ Files.createFile(dir.resolve("foo.c9r"));
+ Files.createFile(dir.resolve("baz.c9r"));
+ Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
+ Node unresolved = new Node(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
+ unresolved.cleartextName = "this is a rather long file name.txt";
+ unresolved.extractedCiphertext = "foo";
+
+ Stream result = conflictResolver.process(unresolved);
+ Assertions.assertTrue(result.findAny().isEmpty());
+ Assertions.assertTrue(Files.exists(unresolved.ciphertextPath));
+ Mockito.verify(fileNameCryptor, Mockito.times(10)).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any());
+ }
+
@Test
public void testResolveConflictingFileTrivially(@TempDir Path dir) throws IOException {
Files.createFile(dir.resolve("foo (1).c9r"));
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
index 05455820..14c60c84 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
@@ -34,9 +34,7 @@ public void setup() {
"aaaaBBBBccccDDDDeeeeFFFF",
"aaaaBBBBccccDDDDeeeeFFF=",
"aaaaBBBBccccDDDDeeeeFF==",
- "aaaaBBBBccccDDDDeeeeF===",
- "aaaaBBBBccccDDDDeeee====",
- "aaaaBBBB0123456789-_====",
+ "aaaaBBBBcc0123456789_-==",
"aaaaBBBBccccDDDDeeeeFFFFggggHH==",
})
public void testValidBase64Pattern(String input) {
@@ -47,7 +45,9 @@ public void testValidBase64Pattern(String input) {
@ValueSource(strings = {
"aaaaBBBBccccDDDDeeee", // too short
"aaaaBBBBccccDDDDeeeeFFF", // unpadded
- "====BBBBccccDDDDeeeeFFFF", // padding not at end
+ "aaaaBBBBccccDDDDeeeeF===", // too much padding
+ "aaaaBBBBccccDDDDeeee====", // too much padding
+ "==aaBBBBccccDDDDeeeeFFFF", // padding not at end
"????BBBBccccDDDDeeeeFFFF", // invalid chars
"conflict aaaaBBBBccccDDDDeeeeFFFF", // only a partial match
"aaaaBBBBccccDDDDeeeeFFFF conflict", // only a partial match