Skip to content

Commit fe7a10d

Browse files
authored
Merge pull request #275 from cryptomator/feature/use-original-conflict-suffixes
Use Conflict Suffixes from Ciphertext Files
2 parents ab87592 + 3e8dede commit fe7a10d

File tree

6 files changed

+246
-40
lines changed

6 files changed

+246
-40
lines changed

pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
<!-- test dependencies -->
2929
<junit.jupiter.version>5.11.3</junit.jupiter.version>
30+
<jmh.version>1.37</jmh.version>
3031
<mockito.version>5.14.2</mockito.version>
3132
<hamcrest.version>3.0</hamcrest.version>
3233
<jimfs.version>1.3.0</jimfs.version>
@@ -123,6 +124,18 @@
123124
<version>${junit.jupiter.version}</version>
124125
<scope>test</scope>
125126
</dependency>
127+
<dependency>
128+
<groupId>org.openjdk.jmh</groupId>
129+
<artifactId>jmh-core</artifactId>
130+
<version>${jmh.version}</version>
131+
<scope>test</scope>
132+
</dependency>
133+
<dependency>
134+
<groupId>org.openjdk.jmh</groupId>
135+
<artifactId>jmh-generator-annprocess</artifactId>
136+
<version>${jmh.version}</version>
137+
<scope>provided</scope> <!-- only required during compilation -->
138+
</dependency>
126139
<dependency>
127140
<groupId>org.mockito</groupId>
128141
<artifactId>mockito-core</artifactId>
@@ -163,6 +176,11 @@
163176
<artifactId>dagger-compiler</artifactId>
164177
<version>${dagger.version}</version>
165178
</path>
179+
<path>
180+
<groupId>org.openjdk.jmh</groupId>
181+
<artifactId>jmh-generator-annprocess</artifactId>
182+
<version>${jmh.version}</version>
183+
</path>
166184
</annotationProcessorPaths>
167185
</configuration>
168186
</plugin>

src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public Stream<Node> process(Node node) {
6161
Path canonicalPath = node.ciphertextPath.resolveSibling(canonicalCiphertextFileName);
6262
return resolveConflict(node, canonicalPath);
6363
} catch (IOException e) {
64-
LOG.error("Failed to resolve conflict for " + node.ciphertextPath, e);
64+
LOG.error("Failed to resolve conflict for {}", node.ciphertextPath, e);
6565
return Stream.empty();
6666
}
6767
}
@@ -75,43 +75,64 @@ private Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throw
7575
resolved.extractedCiphertext = conflicting.extractedCiphertext;
7676
return Stream.of(resolved);
7777
} else {
78-
return Stream.of(renameConflictingFile(canonicalPath, conflictingPath, conflicting.cleartextName));
78+
return renameConflictingFile(canonicalPath, conflicting);
7979
}
8080
}
8181

8282
/**
8383
* Resolves a conflict by renaming the conflicting file.
8484
*
8585
* @param canonicalPath The path to the original (conflict-free) file.
86-
* @param conflictingPath The path to the potentially conflicting file.
87-
* @param cleartext The cleartext name of the conflicting file.
88-
* @return The newly created Node after renaming the conflicting file.
89-
* @throws IOException
86+
* @param conflicting The conflicting file.
87+
* @return The newly created Node if rename succeeded or an empty stream otherwise.
88+
* @throws IOException If an unexpected I/O exception occurs during rename
9089
*/
91-
private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException {
90+
private Stream<Node> renameConflictingFile(Path canonicalPath, Node conflicting) throws IOException {
9291
assert Files.exists(canonicalPath);
93-
final int beginOfFileExtension = cleartext.lastIndexOf('.');
94-
final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : "";
95-
final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext;
96-
final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextFileNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
97-
String alternativeCleartext;
98-
String alternativeCiphertext;
99-
String alternativeCiphertextName;
100-
Path alternativePath;
101-
int i = 1;
102-
do {
103-
alternativeCleartext = lengthRestrictedBasename + " (" + i++ + ")" + fileExtension;
92+
assert conflicting.fullCiphertextFileName.endsWith(Constants.CRYPTOMATOR_FILE_SUFFIX);
93+
assert conflicting.fullCiphertextFileName.contains(conflicting.extractedCiphertext);
94+
95+
final String cleartext = conflicting.cleartextName;
96+
final int beginOfCleartextExt = cleartext.lastIndexOf('.');
97+
final String cleartextFileExt = (beginOfCleartextExt > 0) ? cleartext.substring(beginOfCleartextExt) : "";
98+
final String cleartextBasename = (beginOfCleartextExt > 0) ? cleartext.substring(0, beginOfCleartextExt) : cleartext;
99+
100+
// let's assume that some the sync conflict string is added at the end of the file name, but before .c9r:
101+
final int endOfCiphertext = conflicting.fullCiphertextFileName.indexOf(conflicting.extractedCiphertext) + conflicting.extractedCiphertext.length();
102+
final String originalConflictSuffix = conflicting.fullCiphertextFileName.substring(endOfCiphertext, conflicting.fullCiphertextFileName.length() - Constants.CRYPTOMATOR_FILE_SUFFIX.length());
103+
104+
// split available maxCleartextFileNameLength between basename, conflict suffix, and file extension:
105+
final int netCleartext = maxCleartextFileNameLength - cleartextFileExt.length(); // file extension must be preserved
106+
final String conflictSuffix = originalConflictSuffix.substring(0, Math.min(originalConflictSuffix.length(), netCleartext / 2)); // max 50% of available space
107+
final int conflictSuffixLen = Math.max(4, conflictSuffix.length()); // prefer to use original conflict suffix, but reserver at least 4 chars for numerical fallback: " (9)"
108+
final String lengthRestrictedBasename = cleartextBasename.substring(0, Math.min(cleartextBasename.length(), netCleartext - conflictSuffixLen)); // remaining space for basename
109+
110+
// attempt to use original conflict suffix:
111+
String alternativeCleartext = lengthRestrictedBasename + conflictSuffix + cleartextFileExt;
112+
String alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId);
113+
String alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
114+
Path alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
115+
116+
// fallback to number conflic suffix, if file with alternative path already exists:
117+
for (int i = 1; i < 10 && Files.exists(alternativePath); i++) {
118+
alternativeCleartext = lengthRestrictedBasename + " (" + i + ")" + cleartextFileExt;
104119
alternativeCiphertext = cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), alternativeCleartext, dirId);
105120
alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
106121
alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
107-
} while (Files.exists(alternativePath));
122+
}
123+
108124
assert alternativeCiphertextName.length() <= maxC9rFileNameLength;
109-
LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
110-
Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
125+
if (Files.exists(alternativePath)) {
126+
LOG.warn("Failed finding alternative name for {}. Keeping original name.", conflicting.ciphertextPath);
127+
return Stream.empty();
128+
}
129+
130+
Files.move(conflicting.ciphertextPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
131+
LOG.info("Renamed conflicting file {} to {}...", conflicting.ciphertextPath, alternativePath);
111132
Node node = new Node(alternativePath);
112133
node.cleartextName = alternativeCleartext;
113134
node.extractedCiphertext = alternativeCiphertext;
114-
return node;
135+
return Stream.of(node);
115136
}
116137

117138

src/main/java/org/cryptomator/cryptofs/dir/C9rDecryptor.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
class C9rDecryptor {
2121

2222
// visible for testing:
23-
static final Pattern BASE64_PATTERN = Pattern.compile("([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}");
23+
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}==)");
2424
private static final CharMatcher DELIM_MATCHER = CharMatcher.anyOf("_-");
2525

2626
private final Cryptor cryptor;
@@ -36,11 +36,7 @@ public Stream<Node> process(Node node) {
3636
String basename = StringUtils.removeEnd(node.fullCiphertextFileName, Constants.CRYPTOMATOR_FILE_SUFFIX);
3737
Matcher matcher = BASE64_PATTERN.matcher(basename);
3838
Optional<Node> match = extractCiphertext(node, matcher, 0, basename.length());
39-
if (match.isPresent()) {
40-
return Stream.of(match.get());
41-
} else {
42-
return Stream.empty();
43-
}
39+
return match.stream();
4440
}
4541

4642
private Optional<Node> extractCiphertext(Node node, Matcher matcher, int start, int end) {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package org.cryptomator.cryptofs.dir;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Disabled;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.ValueSource;
8+
import org.openjdk.jmh.annotations.Benchmark;
9+
import org.openjdk.jmh.annotations.BenchmarkMode;
10+
import org.openjdk.jmh.annotations.Fork;
11+
import org.openjdk.jmh.annotations.Level;
12+
import org.openjdk.jmh.annotations.Measurement;
13+
import org.openjdk.jmh.annotations.Mode;
14+
import org.openjdk.jmh.annotations.OutputTimeUnit;
15+
import org.openjdk.jmh.annotations.Param;
16+
import org.openjdk.jmh.annotations.Scope;
17+
import org.openjdk.jmh.annotations.Setup;
18+
import org.openjdk.jmh.annotations.State;
19+
import org.openjdk.jmh.annotations.Warmup;
20+
import org.openjdk.jmh.infra.Blackhole;
21+
import org.openjdk.jmh.runner.Runner;
22+
import org.openjdk.jmh.runner.RunnerException;
23+
import org.openjdk.jmh.runner.options.Options;
24+
import org.openjdk.jmh.runner.options.OptionsBuilder;
25+
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.regex.Matcher;
28+
import java.util.regex.Pattern;
29+
30+
/**
31+
* Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
32+
*/
33+
public class Base64UrlRegexTest {
34+
35+
private static final String[] TEST_INPUTS = { //
36+
"aaaaBBBBccccDDDDeeeeFFFF",
37+
"aaaaBBBBccccDDDDeeeeFFF=",
38+
"aaaaBBBBccccDDDDeeeeFF==",
39+
"aaaaBBBBcc0123456789_-==",
40+
"aaaaBBBBccccDDDDeeeeFFFFggggHH==",
41+
"-3h6-FSsYhMCJHSAV9cjJ89F7R73b0zIB4vvO01b7uWq28fWioRagWpMv-w0MA-2ORjbShuv", //
42+
"iJYw7QpVjKTDv_b6H5jLkauVrnPcGbV9DnIG426EBzjlYmRuJDX5cjFJLmTDA7EOEmo5rAHT3Jc=", //
43+
"PnBpirtqhCKm9hE1341rxkqdASfyYJqNHROxR1xJWDH6TGbeqqXn_sr2Rk5zzVpSbufkiqZH9a==", //
44+
"S8cmirjkHBHbIJZXExbFyyTOA8r6TvTPddK_sdQZpcE3RCMDI0mo9w2f53DSkqT0xRf1xcrmxvU=" //
45+
};
46+
47+
private static final String[] TEST_INVALID_INPUTS = { //
48+
"aaaaBBBBccccDDDDeeee", // too short
49+
"aaaaBBBBccccDDDDeeeeFFF", // unpadded
50+
"====BBBBccccDDDDeeeeFFFF", // padding not at end
51+
"????BBBBccccDDDDeeeeFFFF", // invalid chars
52+
"conflict aaaaBBBBccccDDDDeeeeFFFF", // only a partial match
53+
"aaaaBBBBccccDDDDeeeeFFFF conflict", // only a partial match
54+
"-3h6-FSsYhMCJHSAV9cjJ89F7R73b0zIB4vvO01b7uWq28fWioRagWpMv-w0MA-2ORjbShu", // not multiple of four
55+
"=iJYw7QpVjKTDv_b6H5jLkauVrnPcGbV9DnIG426EBzjlYmRuJDX5cjFJLmTDA7EOEmo5rAHT3J=", // padding in wrong position
56+
"PnBp.irtqhCKm9hE1341rxkqdASfyYJqNHROxR1xJWDH6TGbeqqXn_sr2Rk5zzVpSbufkiqZH9a==", // invalid character
57+
};
58+
59+
@ParameterizedTest
60+
@ValueSource(strings = {
61+
"([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
62+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
63+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
64+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
65+
"[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
66+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
67+
})
68+
public void testValidBase64Pattern(String patternString) {
69+
Pattern pattern = Pattern.compile(patternString);
70+
for (String input : TEST_INPUTS) {
71+
Matcher matcher = pattern.matcher(input);
72+
Assertions.assertTrue(matcher.matches(), "pattern does not match " + input);
73+
}
74+
}
75+
76+
@ParameterizedTest
77+
@ValueSource(strings = {
78+
"([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
79+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
80+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
81+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
82+
"[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
83+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
84+
})
85+
public void testInvalidInputs(String patternString) {
86+
Pattern pattern = Pattern.compile(patternString);
87+
for (String input : TEST_INVALID_INPUTS) {
88+
Matcher matcher = pattern.matcher(input);
89+
Assertions.assertFalse(matcher.matches(), "pattern matches " + input);
90+
}
91+
}
92+
93+
@Test
94+
@Disabled // only run manually
95+
public void runBenchmarks() throws RunnerException {
96+
// Taken from http://stackoverflow.com/a/30486197/4014509:
97+
Options opt = new OptionsBuilder().include(RegexBenchmark.class.getSimpleName()).build();
98+
new Runner(opt).run();
99+
}
100+
101+
@State(Scope.Thread)
102+
@Fork(3)
103+
@Warmup(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS)
104+
@Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)
105+
@BenchmarkMode(value = {Mode.AverageTime})
106+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
107+
public static class RegexBenchmark {
108+
109+
// Base64 regex pattern
110+
@Param({
111+
"([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_]{20}[a-zA-Z0-9-_=]{4}",
112+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
113+
"[a-zA-Z0-9-_]{20}([a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
114+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_]{4})*[a-zA-Z0-9-_=]{4}",
115+
"[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
116+
"[a-zA-Z0-9-_]{20}(?:[a-zA-Z0-9-_=]{4})+", // most permissive
117+
})
118+
private String patternString;
119+
120+
private Pattern pattern;
121+
122+
@Setup(Level.Trial)
123+
public void compilePattern() {
124+
pattern = Pattern.compile(patternString);
125+
}
126+
127+
@Benchmark
128+
public void testPattern(Blackhole bh) {
129+
for (String input : TEST_INPUTS) {
130+
Matcher matcher = pattern.matcher(input);
131+
bh.consume(matcher.matches());
132+
}
133+
}
134+
135+
}
136+
}

src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.junit.jupiter.api.Test;
99
import org.junit.jupiter.api.io.TempDir;
1010
import org.junit.jupiter.params.ParameterizedTest;
11-
import org.junit.jupiter.params.provider.CsvSource;
1211
import org.junit.jupiter.params.provider.ValueSource;
1312
import org.mockito.Mockito;
1413

@@ -31,7 +30,7 @@ public void setup() {
3130
fileNameCryptor = Mockito.mock(FileNameCryptor.class);
3231
vaultConfig = Mockito.mock(VaultConfig.class);
3332
Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
34-
Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(44); // results in max cleartext size = 14
33+
Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(84); // results in max cleartext size = 44
3534
conflictResolver = new C9rConflictResolver(cryptor, "foo", vaultConfig);
3635
}
3736

@@ -60,10 +59,10 @@ public void testResolveHiddenNode(String filename) {
6059

6160
@Test
6261
public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throws IOException {
63-
Files.createFile(dir.resolve("foo (1).c9r"));
62+
Files.createFile(dir.resolve("foo (Created by Alice).c9r"));
6463
Files.createFile(dir.resolve("foo.c9r"));
6564
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
66-
Node unresolved = new Node(dir.resolve("foo (1).c9r"));
65+
Node unresolved = new Node(dir.resolve("foo (Created by Alice).c9r"));
6766
unresolved.cleartextName = "bar.txt";
6867
unresolved.extractedCiphertext = "foo";
6968

@@ -72,30 +71,66 @@ public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throw
7271

7372
Assertions.assertNotEquals(unresolved, resolved);
7473
Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName);
74+
Assertions.assertEquals("bar (Created by Alice).txt", resolved.cleartextName);
75+
Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
76+
Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
77+
}
78+
79+
@Test
80+
public void testResolveConflictingFileByAddingNumericSuffix(@TempDir Path dir) throws IOException {
81+
Files.createFile(dir.resolve("foo (Created by Alice).c9r"));
82+
Files.createFile(dir.resolve("foo.c9r"));
83+
Files.createFile(dir.resolve("baz.c9r")); // resolved name already occupied, try cux next!
84+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz").thenReturn("qux");
85+
Node unresolved = new Node(dir.resolve("foo (Created by Alice).c9r"));
86+
unresolved.cleartextName = "bar.txt";
87+
unresolved.extractedCiphertext = "foo";
88+
89+
Stream<Node> result = conflictResolver.process(unresolved);
90+
Node resolved = result.findAny().get();
91+
92+
Assertions.assertNotEquals(unresolved, resolved);
93+
Assertions.assertEquals("qux.c9r", resolved.fullCiphertextFileName);
7594
Assertions.assertEquals("bar (1).txt", resolved.cleartextName);
7695
Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
7796
Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
7897
}
7998

8099
@Test
81100
public void testResolveConflictingFileByChoosingNewLengthLimitedName(@TempDir Path dir) throws IOException {
82-
Files.createFile(dir.resolve("foo (1).c9r"));
101+
Files.createFile(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
83102
Files.createFile(dir.resolve("foo.c9r"));
84103
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
85-
Node unresolved = new Node(dir.resolve("foo (1).c9r"));
86-
unresolved.cleartextName = "hello world.txt";
104+
Node unresolved = new Node(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
105+
unresolved.cleartextName = "this is a rather long file name.txt";
87106
unresolved.extractedCiphertext = "foo";
88107

89108
Stream<Node> result = conflictResolver.process(unresolved);
90109
Node resolved = result.findAny().get();
91110

92111
Assertions.assertNotEquals(unresolved, resolved);
93112
Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName);
94-
Assertions.assertEquals("hello (1).txt", resolved.cleartextName);
113+
Assertions.assertEquals("this is a rather lon (Created by Alice o.txt", resolved.cleartextName);
95114
Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
96115
Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
97116
}
98117

118+
@Test
119+
public void testResolveConflictFailedAlternativeNamesReserved(@TempDir Path dir) throws IOException {
120+
Files.createFile(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
121+
Files.createFile(dir.resolve("foo.c9r"));
122+
Files.createFile(dir.resolve("baz.c9r"));
123+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
124+
Node unresolved = new Node(dir.resolve("foo (Created by Alice on 2024-01-31).c9r"));
125+
unresolved.cleartextName = "this is a rather long file name.txt";
126+
unresolved.extractedCiphertext = "foo";
127+
128+
Stream<Node> result = conflictResolver.process(unresolved);
129+
Assertions.assertTrue(result.findAny().isEmpty());
130+
Assertions.assertTrue(Files.exists(unresolved.ciphertextPath));
131+
Mockito.verify(fileNameCryptor, Mockito.times(10)).encryptFilename(Mockito.any(), Mockito.any(), Mockito.any());
132+
}
133+
99134
@Test
100135
public void testResolveConflictingFileTrivially(@TempDir Path dir) throws IOException {
101136
Files.createFile(dir.resolve("foo (1).c9r"));

0 commit comments

Comments
 (0)