Skip to content

Commit 84e7167

Browse files
committed
invalidate/move also all cleartext path cache entries starting with the base invalidated path
1 parent 6302574 commit 84e7167

File tree

2 files changed

+123
-5
lines changed

2 files changed

+123
-5
lines changed

src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.nio.file.Path;
2929
import java.nio.file.attribute.BasicFileAttributes;
3030
import java.time.Duration;
31+
import java.util.ArrayList;
32+
import java.util.Map;
3133
import java.util.Objects;
3234
import java.util.Optional;
3335
import java.util.concurrent.CompletableFuture;
@@ -140,14 +142,20 @@ private String getCiphertextFileName(DirIdAndName dirIdAndName) {
140142
}
141143

142144
public void invalidatePathMapping(CryptoPath cleartextPath) {
143-
ciphertextDirectories.asMap().remove(cleartextPath);
145+
ciphertextDirectories.asMap().keySet().removeIf(p -> p.startsWith(cleartextPath));
144146
}
145147

146148
public void movePathMapping(CryptoPath cleartextSrc, CryptoPath cleartextDst) {
147-
var cachedValue = ciphertextDirectories.asMap().remove(cleartextSrc);
148-
if (cachedValue != null) {
149-
ciphertextDirectories.put(cleartextDst, cachedValue);
150-
}
149+
var remappedEntries = new ArrayList<Map.Entry<CryptoPath, CompletableFuture<CiphertextDirectory>>>();
150+
ciphertextDirectories.asMap().entrySet().removeIf(e -> {
151+
if (e.getKey().startsWith(cleartextSrc)) {
152+
var remappedPath = cleartextDst.resolve(cleartextSrc.relativize(e.getKey()));
153+
return remappedEntries.add(Map.entry(remappedPath, e.getValue()));
154+
} else {
155+
return false;
156+
}
157+
});
158+
remappedEntries.forEach(e -> ciphertextDirectories.put(e.getKey(), e.getValue()));
151159
}
152160

153161
public CiphertextDirectory getCiphertextDir(CryptoPath cleartextPath) throws IOException {

src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,116 @@ public void setup() {
6464
Mockito.when(fileSystem.getEmptyPath()).thenReturn(empty);
6565
}
6666

67+
@Test
68+
@DisplayName("Removing a cached cleartext path also removes all cached child paths")
69+
public void testInvalidatingCleartextPathCleansCacheFromChildPaths() throws IOException {
70+
//prepare root
71+
Path d00 = Mockito.mock(Path.class);
72+
Mockito.when(dataRoot.resolve("00")).thenReturn(d00);
73+
Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000");
74+
75+
//prepare cleartextDir "/foo"
76+
Path d0000 = Mockito.mock(Path.class, "d/00/00");
77+
Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r");
78+
Path d0000oofdir = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r");
79+
Mockito.when(d00.resolve("00")).thenReturn(d0000);
80+
Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof);
81+
Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir);
82+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof");
83+
Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1");
84+
Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001");
85+
86+
//prepare cleartextDir "/foo/bar"
87+
Path d0001 = Mockito.mock(Path.class, "d/00/01");
88+
Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r");
89+
Path d0000rabdir = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r");
90+
Mockito.when(d00.resolve("01")).thenReturn(d0001);
91+
Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab);
92+
Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir);
93+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab");
94+
Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2");
95+
Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002");
96+
97+
Path d0002 = Mockito.mock(Path.class);
98+
Mockito.when(d00.resolve("02")).thenReturn(d0002);
99+
100+
CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
101+
//put cleartextpath /foo
102+
Path cipherFooPath = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path;
103+
//put cleartextpath /foo/bar
104+
Path cipherFooBarPath = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path;
105+
//invalidate /foo
106+
mapper.invalidatePathMapping(fileSystem.getPath("/foo"));
107+
//cache should miss
108+
var mapperSpy = Mockito.spy(mapper);
109+
mapperSpy.getCiphertextDir(fileSystem.getPath("/foo/bar"));
110+
Mockito.verify(mapperSpy, Mockito.atLeast(1)).getCiphertextFilePath(Mockito.any());
111+
}
112+
113+
@Test
114+
@DisplayName("Moving a cached cleartext path also remaps all cached child paths")
115+
public void testMovingCleartextPathRemapsCachedChildPaths() throws IOException {
116+
CryptoPath fooPath = fileSystem.getPath("/foo");
117+
CryptoPath fooBarPath = fileSystem.getPath("/foo/bar");
118+
CryptoPath unkelFooPath = fileSystem.getPath("/unkel/foo");
119+
CryptoPath unkelFooBarPath = fileSystem.getPath("/unkel/foo/bar");
120+
//prepare root
121+
Path d00 = Mockito.mock(Path.class);
122+
Mockito.when(dataRoot.resolve("00")).thenReturn(d00);
123+
Mockito.when(fileNameCryptor.hashDirectoryId("")).thenReturn("0000");
124+
125+
//prepare cleartextDir "/foo"
126+
Path d0000 = Mockito.mock(Path.class, "d/00/00");
127+
Path d0000oof = Mockito.mock(Path.class, "d/00/00/oof.c9r");
128+
Path d0000oofdir = Mockito.mock(Path.class, "d/00/00/oof.c9r/dir.c9r");
129+
Mockito.when(d00.resolve("00")).thenReturn(d0000);
130+
Mockito.when(d0000.resolve("oof.c9r")).thenReturn(d0000oof);
131+
Mockito.when(d0000oof.resolve("dir.c9r")).thenReturn(d0000oofdir);
132+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("oof");
133+
Mockito.when(dirIdProvider.load(d0000oofdir)).thenReturn("1");
134+
Mockito.when(fileNameCryptor.hashDirectoryId("1")).thenReturn("0001");
135+
136+
//prepare cleartextDir "/foo/bar"
137+
Path d0001 = Mockito.mock(Path.class, "d/00/01");
138+
Path d0001rab = Mockito.mock(Path.class, "d/00/01/rab.c9r");
139+
Path d0000rabdir = Mockito.mock(Path.class, "d/00/00/rab.c9r/dir.c9r");
140+
Mockito.when(d00.resolve("01")).thenReturn(d0001);
141+
Mockito.when(d0001.resolve("rab.c9r")).thenReturn(d0001rab);
142+
Mockito.when(d0001rab.resolve("dir.c9r")).thenReturn(d0000rabdir);
143+
Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("bar"), Mockito.any())).thenReturn("rab");
144+
Mockito.when(dirIdProvider.load(d0000rabdir)).thenReturn("2");
145+
Mockito.when(fileNameCryptor.hashDirectoryId("2")).thenReturn("0002");
146+
147+
Path d0002 = Mockito.mock(Path.class);
148+
Mockito.when(d00.resolve("02")).thenReturn(d0002);
149+
150+
CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, vaultConfig);
151+
//put cleartextpath /foo in cache
152+
Path cipherFooPath = mapper.getCiphertextDir(fooPath).path;
153+
//put cleartextpath /foo/bar in cache
154+
Path cipherBarPath = mapper.getCiphertextDir(fooBarPath).path;
155+
//move /foo to /unkel/dinkel/foo/, effectively moving also moving /foo/bar
156+
mapper.movePathMapping(fooPath, unkelFooPath);
157+
158+
//cache should ...
159+
var mapperSpy = Mockito.spy(mapper);
160+
var someCiphertextFilePath = Mockito.mock(CiphertextFilePath.class);
161+
var someCiphertextDirFilePath = Mockito.mock(Path.class);
162+
var someCipherDirObj = Mockito.mock(CryptoPathMapper.CiphertextDirectory.class);
163+
Mockito.doReturn(someCiphertextFilePath).when(mapperSpy).getCiphertextFilePath(fooBarPath);
164+
Mockito.doReturn(someCiphertextDirFilePath).when(someCiphertextFilePath).getDirFilePath();
165+
Mockito.doReturn(someCipherDirObj).when(mapperSpy).resolveDirectory(someCiphertextDirFilePath);
166+
167+
//... succeed for /unkel/foo/ and /unkel/foo/bar
168+
mapperSpy.getCiphertextDir(unkelFooPath);
169+
mapperSpy.getCiphertextDir(unkelFooBarPath);
170+
Mockito.verify(mapperSpy, Mockito.never()).getCiphertextFilePath(Mockito.any());
171+
172+
//...miss and return our mocked cipherDirObj
173+
var actualCipherDirObj = mapperSpy.getCiphertextDir(fooBarPath);
174+
Assertions.assertEquals(someCipherDirObj, actualCipherDirObj);
175+
}
176+
67177
@Test
68178
public void testPathEncryptionForRoot() throws IOException {
69179
Path d00 = Mockito.mock(Path.class);

0 commit comments

Comments
 (0)