diff --git a/pom.xml b/pom.xml
index 0083ba02..831af0b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 2.6.6
+ 2.6.7
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index 8bd83d2d..d3d7af7c 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -18,6 +18,7 @@
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
import org.cryptomator.cryptofs.common.FinallyUtil;
import org.cryptomator.cryptofs.dir.CiphertextDirectoryDeleter;
+import org.cryptomator.cryptofs.dir.DirectoryStreamFilters;
import org.cryptomator.cryptofs.dir.DirectoryStreamFactory;
import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
import org.cryptomator.cryptolib.api.Cryptor;
@@ -621,20 +622,21 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
throw new AtomicMoveNotSupportedException(cleartextSource.toString(), cleartextTarget.toString(), "Replacing directories during move requires non-atomic status checks.");
}
// check if dir is empty:
- Path oldCiphertextDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
- boolean oldCiphertextDirExists = true;
- try (DirectoryStream ds = Files.newDirectoryStream(oldCiphertextDir)) {
+ Path targetCiphertextDirContentDir = cryptoPathMapper.getCiphertextDir(cleartextTarget).path;
+ boolean targetCiphertextDirExists = true;
+ try (DirectoryStream ds = Files.newDirectoryStream(targetCiphertextDirContentDir, DirectoryStreamFilters.EXCLUDE_DIR_ID_BACKUP)) {
if (ds.iterator().hasNext()) {
throw new DirectoryNotEmptyException(cleartextTarget.toString());
}
} catch (NoSuchFileException e) {
- oldCiphertextDirExists = false;
- }
- // cleanup dir to be replaced:
- if (oldCiphertextDirExists) {
- Files.walkFileTree(oldCiphertextDir, DeletingFileVisitor.INSTANCE);
+ targetCiphertextDirExists = false;
}
+ //delete dir link
Files.walkFileTree(ciphertextTarget.getRawPath(), DeletingFileVisitor.INSTANCE);
+ // cleanup content dir
+ if (targetCiphertextDirExists) {
+ Files.walkFileTree(targetCiphertextDirContentDir, DeletingFileVisitor.INSTANCE);
+ }
}
// no exceptions until this point, so MOVE:
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java
new file mode 100644
index 00000000..13f87c11
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/dir/DirectoryStreamFilters.java
@@ -0,0 +1,12 @@
+package org.cryptomator.cryptofs.dir;
+
+import org.cryptomator.cryptofs.common.Constants;
+
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+
+public interface DirectoryStreamFilters {
+
+ static DirectoryStream.Filter EXCLUDE_DIR_ID_BACKUP = p -> !p.equals(p.resolveSibling(Constants.DIR_BACKUP_FILE_NAME));
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java
new file mode 100644
index 00000000..6b494ebb
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderInMemoryIntegrationTest.java
@@ -0,0 +1,103 @@
+package org.cryptomator.cryptofs;
+
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Set;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CryptoFileSystemProviderInMemoryIntegrationTest {
+
+ private static FileSystem tmpFs;
+ private static Path pathToVault;
+
+ @BeforeAll
+ public static void beforeAll() {
+ tmpFs = Jimfs.newFileSystem(Configuration.unix());
+ pathToVault = tmpFs.getPath("/vault");
+ }
+
+ @BeforeEach
+ public void beforeEach() throws IOException {
+ Files.createDirectory(pathToVault);
+ }
+
+ @AfterEach
+ public void afterEach() throws IOException {
+ try (var paths = Files.walk(pathToVault)) {
+ var nodes = paths.sorted(Comparator.reverseOrder()).toList();
+ for (var node : nodes) {
+ Files.delete(node);
+ }
+ }
+ }
+
+ @AfterAll
+ public static void afterAll() throws IOException {
+ tmpFs.close();
+ }
+
+ @Test
+ @DisplayName("Replace an existing, shortened, empty directory")
+ public void testReplaceExistingShortenedDirEmpty() throws IOException {
+ try (var fs = setupCryptoFs(50, 100, false)) {
+ var dirName50Chars = "/target_89_123456789_123456789_123456789_123456789_"; //since filename encryption increases filename length, 50 cleartext chars are sufficient
+ var source = fs.getPath("/sourceDir");
+ var target = fs.getPath(dirName50Chars);
+ Files.createDirectory(source);
+ Files.createDirectory(target);
+ assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING));
+ assertTrue(Files.notExists(source));
+ assertTrue(Files.exists(target));
+ }
+ }
+
+ @Test
+ @DisplayName("Replace an existing, shortened file")
+ public void testReplaceExistingShortenedFile() throws IOException {
+ try (var fs = setupCryptoFs(50, 100, false)) {
+ var fiftyCharName2 = "/50char2_50char2_50char2_50char2_50char2_50char.txt"; //since filename encryption increases filename length, 50 cleartext chars are sufficient
+ var source = fs.getPath("/source.txt");
+ var target = fs.getPath(fiftyCharName2);
+ Files.createFile(source);
+ Files.createFile(target);
+
+ assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING));
+ assertTrue(Files.notExists(source));
+ assertTrue(Files.exists(target));
+ }
+ }
+
+ private FileSystem setupCryptoFs(int ciphertextShorteningThreshold, int maxCleartextFilename, boolean readonly) throws IOException {
+ byte[] key = new byte[64];
+ Arrays.fill(key, (byte) 0x55);
+ var keyLoader = Mockito.mock(MasterkeyLoader.class);
+ Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key));
+ var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).withShorteningThreshold(ciphertextShorteningThreshold).withMaxCleartextNameLength(maxCleartextFilename).withFlags(readonly ? Set.of(CryptoFileSystemProperties.FileSystemFlags.READONLY) : Set.of()).build();
+ CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
+ URI fsUri = CryptoFileSystemUri.create(pathToVault);
+ return FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index c9f0b4bd..952a79f6 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -1,844 +1,782 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
-package org.cryptomator.cryptofs;
-
-import com.google.common.io.MoreFiles;
-import com.google.common.jimfs.Configuration;
-import com.google.common.jimfs.Jimfs;
-import org.cryptomator.cryptofs.ch.CleartextFileChannel;
-import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.api.MasterkeyLoader;
-import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
-import org.hamcrest.MatcherAssert;
-import org.hamcrest.Matchers;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Assumptions;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.MethodOrderer;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.api.TestMethodOrder;
-import org.junit.jupiter.api.condition.EnabledOnOs;
-import org.junit.jupiter.api.condition.OS;
-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;
-
-import java.io.IOException;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import java.nio.channels.NonReadableChannelException;
-import java.nio.channels.NonWritableChannelException;
-import java.nio.channels.OverlappingFileLockException;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.DosFileAttributeView;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.Set;
-
-import static java.nio.file.Files.readAllBytes;
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
-import static org.hamcrest.Matchers.is;
-
-
-public class CryptoFileSystemProviderIntegrationTest {
-
- @Nested
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- public class WithLimitedPaths {
-
- private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
- private CryptoFileSystem fs;
- private Path shortFilePath;
- private Path shortSymlinkPath;
- private Path shortDirPath;
-
- @BeforeAll
- public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
- Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
- CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
- .withFlags() //
- .withMasterkeyFilename("masterkey.cryptomator") //
- .withKeyLoader(keyLoader) //
- .withMaxCleartextNameLength(50)
- .build();
- CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
- fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
- }
-
- @BeforeEach
- public void setupEach() throws IOException {
- shortFilePath = fs.getPath("/short-enough.txt");
- shortDirPath = fs.getPath("/short-enough-dir");
- shortSymlinkPath = fs.getPath("/symlink.txt");
- Files.createFile(shortFilePath);
- Files.createDirectory(shortDirPath);
- Files.createSymbolicLink(shortSymlinkPath, shortFilePath);
- }
-
- @AfterEach
- public void tearDownEach() throws IOException {
- Files.deleteIfExists(shortFilePath);
- Files.deleteIfExists(shortDirPath);
- Files.deleteIfExists(shortSymlinkPath);
- }
-
- @DisplayName("expect create file to fail with FileNameTooLongException")
- @Test
- public void testCreateFileExceedingPathLengthLimit() {
- Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
- Assertions.assertThrows(FileNameTooLongException.class, () -> {
- Files.createFile(p);
- });
- }
-
- @DisplayName("expect create directory to fail with FileNameTooLongException")
- @Test
- public void testCreateDirExceedingPathLengthLimit() {
- Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
- Assertions.assertThrows(FileNameTooLongException.class, () -> {
- Files.createDirectory(p);
- });
- }
-
- @DisplayName("expect create symlink to fail with FileNameTooLongException")
- @Test
- public void testCreateSymlinkExceedingPathLengthLimit() {
- Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
- Assertions.assertThrows(FileNameTooLongException.class, () -> {
- Files.createSymbolicLink(p, shortFilePath);
- });
- }
-
- @DisplayName("expect move to fail with FileNameTooLongException")
- @ParameterizedTest(name = "move {0} -> this-cleartext-filename-is-longer-than-50-characters")
- @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
- public void testMoveExceedingPathLengthLimit(String path) {
- Path src = fs.getPath(path);
- Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
- Assertions.assertThrows(FileNameTooLongException.class, () -> {
- Files.move(src, dst);
- });
- Assertions.assertTrue(Files.exists(src));
- Assertions.assertTrue(Files.notExists(dst));
- }
-
- @DisplayName("expect copy to fail with FileNameTooLongException")
- @ParameterizedTest(name = "copy {0} -> this-cleartext-filename-is-longer-than-50-characters")
- @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
- public void testCopyExceedingPathLengthLimit(String path) {
- Path src = fs.getPath(path);
- Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
- Assertions.assertThrows(FileNameTooLongException.class, () -> {
- Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS);
- });
- Assertions.assertTrue(Files.exists(src));
- Assertions.assertTrue(Files.notExists(dst));
- }
-
- }
-
- @Nested
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
- public class InMemoryOrdered {
-
- private FileSystem tmpFs;
- private MasterkeyLoader keyLoader1;
- private MasterkeyLoader keyLoader2;
- private Path pathToVault1;
- private Path pathToVault2;
- private Path vaultConfigFile1;
- private Path vaultConfigFile2;
- private FileSystem fs1;
- private FileSystem fs2;
-
- @BeforeAll
- public void setup() throws IOException, MasterkeyLoadingFailedException {
- tmpFs = Jimfs.newFileSystem(Configuration.unix());
- byte[] key1 = new byte[64];
- byte[] key2 = new byte[64];
- Arrays.fill(key1, (byte) 0x55);
- Arrays.fill(key2, (byte) 0x77);
- keyLoader1 = Mockito.mock(MasterkeyLoader.class);
- keyLoader2 = Mockito.mock(MasterkeyLoader.class);
- Mockito.when(keyLoader1.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key1));
- Mockito.when(keyLoader2.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key2));
- pathToVault1 = tmpFs.getPath("/vaultDir1");
- pathToVault2 = tmpFs.getPath("/vaultDir2");
- Files.createDirectory(pathToVault1);
- Files.createDirectory(pathToVault2);
- vaultConfigFile1 = pathToVault1.resolve("vault.cryptomator");
- vaultConfigFile2 = pathToVault2.resolve("vault.cryptomator");
- }
-
- @AfterAll
- public void teardown() throws IOException {
- tmpFs.close();
- }
-
- @Test
- @Order(1)
- @DisplayName("initialize vaults")
- public void initializeVaults() {
- Assertions.assertAll(
- () -> {
- var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build();
- CryptoFileSystemProvider.initialize(pathToVault1, properties, URI.create("test:key"));
- Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
- Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1));
- }, () -> {
- var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build();
- CryptoFileSystemProvider.initialize(pathToVault2, properties, URI.create("test:key"));
- Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
- Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2));
- });
- }
-
- @Test
- @Order(2)
- @DisplayName("get filesystem with incorrect credentials")
- public void testGetFsWithWrongCredentials() throws IOException {
- Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
- Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
- Assertions.assertAll(
- () -> {
- URI fsUri = CryptoFileSystemUri.create(pathToVault1);
- CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
- .withFlags() //
- .withMasterkeyFilename("masterkey.cryptomator") //
- .withKeyLoader(keyLoader2) //
- .build();
- Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
- FileSystems.newFileSystem(fsUri, properties);
- });
- },
- () -> {
- URI fsUri = CryptoFileSystemUri.create(pathToVault2);
- CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
- .withFlags() //
- .withMasterkeyFilename("masterkey.cryptomator") //
- .withKeyLoader(keyLoader1) //
- .build();
- Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
- FileSystems.newFileSystem(fsUri, properties);
- });
- });
- }
-
- @Test
- @Order(4)
- @DisplayName("get filesystem with correct credentials")
- public void testGetFsViaNioApi() {
- Assumptions.assumeTrue(Files.exists(vaultConfigFile1));
- Assumptions.assumeTrue(Files.exists(vaultConfigFile2));
- Assertions.assertAll(
- () -> {
- URI fsUri = CryptoFileSystemUri.create(pathToVault1);
- fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build());
- Assertions.assertTrue(fs1 instanceof CryptoFileSystemImpl);
-
- FileSystem sameFs = FileSystems.getFileSystem(fsUri);
- Assertions.assertSame(fs1, sameFs);
- },
- () -> {
- URI fsUri = CryptoFileSystemUri.create(pathToVault2);
- fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build());
- Assertions.assertTrue(fs2 instanceof CryptoFileSystemImpl);
-
- FileSystem sameFs = FileSystems.getFileSystem(fsUri);
- Assertions.assertSame(fs2, sameFs);
- });
- }
-
- @Test
- @Order(5)
- @DisplayName("touch /foo")
- public void testOpenAndCloseFileChannel() throws IOException {
- Assumptions.assumeTrue(fs1.isOpen());
-
- try (FileChannel ch = FileChannel.open(fs1.getPath("/foo"), EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW))) {
- Assertions.assertTrue(ch instanceof CleartextFileChannel);
- }
- }
-
- @Test
- @Order(6)
- @DisplayName("ln -s foo /link")
- public void testCreateSymlink() {
- Path target = fs1.getPath("/foo");
- Assumptions.assumeTrue(Files.isRegularFile(target));
- Path link = fs1.getPath("/link");
-
- Assertions.assertDoesNotThrow(() -> {
- Files.createSymbolicLink(link, target);
- });
- }
-
- @Test
- @Order(7)
- @DisplayName("echo 'hello world' > /link")
- public void testWriteToSymlink() throws IOException {
- Path link = fs1.getPath("/link");
- Assumptions.assumeTrue(Files.isSymbolicLink(link));
-
- Assertions.assertDoesNotThrow(() -> {
- try (WritableByteChannel ch = Files.newByteChannel(link, StandardOpenOption.WRITE)) {
- ch.write(StandardCharsets.US_ASCII.encode("hello world"));
- }
- });
- }
-
- @Test
- @Order(7)
- @DisplayName("cat `readlink -f /link`")
- public void testReadFromSymlink() throws IOException {
- Path link = fs1.getPath("/link");
- Assumptions.assumeTrue(Files.isSymbolicLink(link));
- Path target = Files.readSymbolicLink(link);
-
- try (ReadableByteChannel ch = Files.newByteChannel(target, StandardOpenOption.READ)) {
- ByteBuffer buf = ByteBuffer.allocate(100);
- ch.read(buf);
- buf.flip();
- String str = StandardCharsets.US_ASCII.decode(buf).toString();
- Assertions.assertEquals("hello world", str);
- }
- }
-
- @Test
- @Order(7)
- @DisplayName("cp /link /otherlink")
- public void testCopySymlinkSymlink() throws IOException {
- Path src = fs1.getPath("/link");
- Path dst = fs1.getPath("/otherlink");
- Assumptions.assumeTrue(Files.isSymbolicLink(src));
- Assumptions.assumeTrue(Files.notExists(dst));
- Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS);
- Assertions.assertTrue(Files.isSymbolicLink(src));
- Assertions.assertTrue(Files.isSymbolicLink(dst));
- }
-
- @Test
- @Order(8)
- @DisplayName("rm /link")
- public void testRemoveSymlink() throws IOException {
- Path link = fs1.getPath("/link");
- Assumptions.assumeTrue(Files.isSymbolicLink(link));
-
- Assertions.assertDoesNotThrow(() -> {
- Files.delete(link);
- });
- }
-
- @Test
- @Order(8)
- @DisplayName("rm /otherlink")
- public void testRemoveOtherSymlink() throws IOException {
- Path link = fs1.getPath("/otherlink");
- Assumptions.assumeTrue(Files.isSymbolicLink(link));
-
- Assertions.assertDoesNotThrow(() -> {
- Files.delete(link);
- });
- }
-
- @Test
- @Order(9)
- @DisplayName("ln -s foo '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
- public void testCreateSymlinkWithLongName() throws IOException {
- Path target = fs1.getPath("/foo");
- Assumptions.assumeTrue(Files.isRegularFile(target));
- Path longNameLink = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Files.createSymbolicLink(longNameLink, target);
- MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNameLink));
- Assertions.assertTrue(Files.exists(longNameLink));
- }
-
- @Test
- @Order(10)
- @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
- public void testMoveSymlinkWithLongNameToAnotherLongName() throws IOException {
- Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Assumptions.assumeTrue(Files.isSymbolicLink(longNameSource));
- Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.move(longNameSource, longNameTarget);
- Assertions.assertTrue(Files.exists(longNameTarget));
- Assertions.assertTrue(Files.notExists(longNameSource));
- }
-
- @Test
- @Order(11)
- @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
- public void testRemoveSymlinkWithLongName() throws IOException {
- Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.delete(longNamePath);
- Assertions.assertTrue(Files.notExists(longNamePath));
- }
- @Test
- @Order(12)
- @DisplayName("mkdir '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
- public void testCreateDirWithLongName() throws IOException {
- Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Files.createDirectory(longNamePath);
- Assertions.assertTrue(Files.isDirectory(longNamePath));
- MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath));
- }
-
- @Test
- @Order(13)
- @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
- public void testMoveDirWithLongNameToAnotherLongName() throws IOException {
- Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.move(longNameSource, longNameTarget);
- Assertions.assertTrue(Files.exists(longNameTarget));
- Assertions.assertTrue(Files.notExists(longNameSource));
- }
-
- @Test
- @Order(14)
- @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
- public void testRemoveDirWithLongName() throws IOException {
- Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.delete(longNamePath);
- Assertions.assertTrue(Files.notExists(longNamePath));
- }
-
- @Test
- @Order(15)
- @DisplayName("touch '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
- public void testCreateFileWithLongName() throws IOException {
- Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Files.createFile(longNamePath);
- Assertions.assertTrue(Files.isRegularFile(longNamePath));
- MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath));
- }
-
- @Test
- @Order(16)
- @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
- public void testMoveFileWithLongNameToAnotherLongName() throws IOException {
- Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
- Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.move(longNameSource, longNameTarget);
- Assertions.assertTrue(Files.exists(longNameTarget));
- Assertions.assertTrue(Files.notExists(longNameSource));
- }
-
- @Test
- @Order(17)
- @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
- public void testRemoveFileWithLongName() throws IOException {
- Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
- Files.delete(longNamePath);
- Assertions.assertTrue(Files.notExists(longNamePath));
- }
-
- @Test
- @Order(18)
- @DisplayName("cp fs1:/foo fs2:/bar")
- public void testCopyFileAcrossFilesystem() throws IOException {
- Path file1 = fs1.getPath("/foo");
- Path file2 = fs2.getPath("/bar");
- Assumptions.assumeTrue(Files.isRegularFile(file1));
- Assumptions.assumeTrue(Files.notExists(file2));
-
- Files.copy(file1, file2);
-
- Assertions.assertArrayEquals(readAllBytes(file1), readAllBytes(file2));
- }
-
- @Test
- @Order(19)
- @DisplayName("echo 'goodbye world' > /foo")
- public void testWriteToFile() throws IOException {
- Path file1 = fs1.getPath("/foo");
- Assumptions.assumeTrue(Files.isRegularFile(file1));
-
- Assertions.assertDoesNotThrow(() -> {
- Files.write(file1, "goodbye world".getBytes());
- });
- }
-
- @Test
- @Order(20)
- @DisplayName("cp -f fs1:/foo fs2:/bar")
- public void testCopyFileAcrossFilesystemReplaceExisting() throws IOException {
- Path file1 = fs1.getPath("/foo");
- Path file2 = fs2.getPath("/bar");
- Assumptions.assumeTrue(Files.isRegularFile(file1));
- Assumptions.assumeTrue(Files.isRegularFile(file2));
-
- Files.copy(file1, file2, REPLACE_EXISTING);
-
- Assertions.assertArrayEquals(readAllBytes(file1), readAllBytes(file2));
- }
-
- @Test
- @Order(21)
- @DisplayName("readattr /attributes.txt")
- public void testLazinessOfFileAttributeViews() throws IOException {
- Path file = fs1.getPath("/attributes.txt");
- Assumptions.assumeTrue(Files.notExists(file));
-
- BasicFileAttributeView attrView = Files.getFileAttributeView(file, BasicFileAttributeView.class);
- Assertions.assertNotNull(attrView);
- Assertions.assertThrows(NoSuchFileException.class, () -> {
- attrView.readAttributes();
- });
-
- Files.write(file, new byte[3], StandardOpenOption.CREATE_NEW);
- BasicFileAttributes attrs = attrView.readAttributes();
- Assertions.assertNotNull(attrs);
- Assertions.assertEquals(3, attrs.size());
-
- Files.delete(file);
- Assertions.assertThrows(NoSuchFileException.class, () -> {
- attrView.readAttributes();
- });
- Assertions.assertEquals(3, attrs.size()); // attrs should be immutable once they are read.
- }
-
- @Test
- @Order(22)
- @DisplayName("ln -s /linked/targetY /links/linkX")
- public void testSymbolicLinks() throws IOException {
- Path linksDir = fs1.getPath("/links");
- Assumptions.assumeTrue(Files.notExists(linksDir));
- Files.createDirectories(linksDir);
-
- Assertions.assertAll(
- () -> {
- Path link = linksDir.resolve("link1");
- Files.createDirectories(link.getParent());
- Files.createSymbolicLink(link, fs1.getPath("/linked/target1"));
- Path target = Files.readSymbolicLink(link);
- MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem())); // as per contract of readSymbolicLink
- MatcherAssert.assertThat(target.toString(), Matchers.equalTo("/linked/target1"));
- MatcherAssert.assertThat(link.resolveSibling(target).toString(), Matchers.equalTo("/linked/target1"));
- },
- () -> {
- Path link = linksDir.resolve("link2");
- Files.createDirectories(link.getParent());
- Files.createSymbolicLink(link, fs1.getPath("./target2"));
- Path target = Files.readSymbolicLink(link);
- MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem()));
- MatcherAssert.assertThat(target.toString(), Matchers.equalTo("./target2"));
- MatcherAssert.assertThat(link.resolveSibling(target).normalize().toString(), Matchers.equalTo("/links/target2"));
- },
- () -> {
- Path link = linksDir.resolve("link3");
- Files.createDirectories(link.getParent());
- Files.createSymbolicLink(link, fs1.getPath("../target3"));
- Path target = Files.readSymbolicLink(link);
- MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem()));
- MatcherAssert.assertThat(target.toString(), Matchers.equalTo("../target3"));
- MatcherAssert.assertThat(link.resolveSibling(target).normalize().toString(), Matchers.equalTo("/target3"));
- }
- );
- }
-
- @Test
- @Order(22)
- @DisplayName("mv -f fs1:/foo fs2:/baz")
- public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException {
- Path file1 = fs1.getPath("/foo");
- Path file2 = fs2.getPath("/baz");
- Assumptions.assumeTrue(Files.isRegularFile(file1));
- Assumptions.assumeTrue(Files.notExists(file2));
- byte[] contents = readAllBytes(file1);
-
- Files.move(file1, file2);
-
- Assertions.assertTrue(Files.notExists(file1));
- Assertions.assertTrue(Files.isRegularFile(file2));
- Assertions.assertArrayEquals(contents, readAllBytes(file2));
- }
-
- }
-
-
- @Nested
- public class InMemory {
-
- private static FileSystem tmpFs;
- private static Path pathToVault;
-
- @BeforeAll
- public static void beforeAll() {
- tmpFs = Jimfs.newFileSystem(Configuration.unix());
- pathToVault = tmpFs.getPath("/vault");
- }
-
- @BeforeEach
- public void beforeEach() throws IOException {
- Files.createDirectory(pathToVault);
- }
-
- @AfterEach
- public void afterEach() throws IOException {
- try (var paths = Files.walk(pathToVault)) {
- var nodes = paths.sorted(Comparator.reverseOrder()).toList();
- for (var node : nodes) {
- Files.delete(node);
- }
- }
- }
-
- @AfterAll
- public static void afterAll() throws IOException {
- tmpFs.close();
- }
-
- @Test
- @DisplayName("Replace an existing, shortened file")
- public void testReplaceExistingShortenedFile() throws IOException {
- try (var fs = setupCryptoFs(50, 100, false)) {
- var fiftyCharName2 = "/50char2_50char2_50char2_50char2_50char2_50char.txt"; //since filename encryption increases filename length, 50 cleartext chars are sufficient
- var source = fs.getPath("/source.txt");
- var target = fs.getPath(fiftyCharName2);
- Files.createFile(source);
- Files.createFile(target);
-
- Assertions.assertDoesNotThrow(() -> Files.move(source, target, REPLACE_EXISTING));
- Assertions.assertTrue(Files.notExists(source));
- Assertions.assertTrue(Files.exists(target));
- }
- }
-
- private FileSystem setupCryptoFs(int ciphertextShorteningThreshold, int maxCleartextFilename, boolean readonly) throws IOException {
- byte[] key = new byte[64];
- Arrays.fill(key, (byte) 0x55);
- var keyLoader = Mockito.mock(MasterkeyLoader.class);
- Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key));
- var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).withShorteningThreshold(ciphertextShorteningThreshold).withMaxCleartextNameLength(maxCleartextFilename).withFlags(readonly ? Set.of(CryptoFileSystemProperties.FileSystemFlags.READONLY) : Set.of()).build();
- CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
- URI fsUri = CryptoFileSystemUri.create(pathToVault);
- return FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
- }
-
- }
-
- @Nested
- @EnabledOnOs({OS.MAC, OS.LINUX})
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- @DisplayName("On POSIX Systems")
- public class PosixTests {
-
- private FileSystem fs;
-
- @BeforeAll
- public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
- Path pathToVault = tmpDir.resolve("vaultDir1");
- Files.createDirectories(pathToVault);
- MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
- Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
- var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
- CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
- fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
- }
-
- @Nested
- @DisplayName("File Locks")
- public class FileLockTests {
-
- private Path file = fs.getPath("/lock.txt");
-
- @BeforeEach
- public void setup() throws IOException {
- Files.write(file, new byte[100000]); // > 3 * 32k
- }
-
- @Test
- @DisplayName("get shared lock on non-readable channel fails")
- public void testGetSharedLockOnNonReadableChannel() throws IOException {
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE)) {
- Assertions.assertThrows(NonReadableChannelException.class, () -> {
- ch.lock(0, 50000, true);
- });
- }
- }
-
- @Test
- @DisplayName("locking a closed channel fails")
- public void testLockClosedChannel() throws IOException {
- FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE);
- ch.close();
- Assertions.assertThrows(ClosedChannelException.class, () -> {
- ch.lock();
- });
- }
-
- @Test
- @DisplayName("get exclusive lock on non-writable channel fails")
- public void testGetSharedLockOnNonWritableChannel() throws IOException {
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
- Assertions.assertThrows(NonWritableChannelException.class, () -> {
- ch.lock(0, 50000, false);
- });
- }
- }
-
- @ParameterizedTest(name = "shared = {0}")
- @CsvSource({"true", "false"})
- @DisplayName("create non-overlapping locks")
- public void testNonOverlappingLocks(boolean shared) throws IOException {
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
- try (FileLock lock1 = ch.lock(0, 10000, shared)) {
- try (FileLock lock2 = ch.lock(90000, 10000, shared)) {
- Assertions.assertNotSame(lock1, lock2);
- }
- }
- }
- }
-
- @ParameterizedTest(name = "shared = {0}")
- @CsvSource({"true", "false"})
- @DisplayName("create overlapping locks")
- public void testOverlappingLocks(boolean shared) throws IOException {
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
- try (FileLock lock1 = ch.lock(0, 10000, shared)) {
- // while bock locks cover different cleartext byte ranges, it is necessary to lock the same ciphertext block
- Assertions.assertThrows(OverlappingFileLockException.class, () -> {
- ch.lock(10000, 10000, shared);
- });
- }
- }
- }
-
- }
-
-
- }
-
- @Nested
- @EnabledOnOs(OS.WINDOWS)
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
- @DisplayName("On Windows Systems")
- public class WindowsTests {
-
- private FileSystem fs;
-
- @BeforeAll
- public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
- Path pathToVault = tmpDir.resolve("vaultDir1");
- Files.createDirectories(pathToVault);
- MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
- Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
- var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
- CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
- fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
- }
-
- @Test
- @DisplayName("set dos attributes")
- public void testDosFileAttributes() throws IOException {
- Path file = fs.getPath("/msDosAttributes.txt");
- Assumptions.assumeTrue(Files.notExists(file));
-
- Files.write(file, new byte[1]);
-
- Files.setAttribute(file, "dos:hidden", true);
- Files.setAttribute(file, "dos:system", true);
- Files.setAttribute(file, "dos:archive", true);
- Files.setAttribute(file, "dos:readOnly", true);
-
- Assertions.assertEquals(true, Files.getAttribute(file, "dos:hidden"));
- Assertions.assertEquals(true, Files.getAttribute(file, "dos:system"));
- Assertions.assertEquals(true, Files.getAttribute(file, "dos:archive"));
- Assertions.assertEquals(true, Files.getAttribute(file, "dos:readOnly"));
-
- Files.setAttribute(file, "dos:hidden", false);
- Files.setAttribute(file, "dos:system", false);
- Files.setAttribute(file, "dos:archive", false);
- Files.setAttribute(file, "dos:readOnly", false);
-
- Assertions.assertEquals(false, Files.getAttribute(file, "dos:hidden"));
- Assertions.assertEquals(false, Files.getAttribute(file, "dos:system"));
- Assertions.assertEquals(false, Files.getAttribute(file, "dos:archive"));
- Assertions.assertEquals(false, Files.getAttribute(file, "dos:readOnly"));
- }
-
- @Nested
- @DisplayName("read-only file")
- public class OnReadOnlyFile {
-
- private Path file = fs.getPath("/readonly.txt");
- private DosFileAttributeView attrView;
-
- @BeforeEach
- public void setup() throws IOException {
- Files.write(file, new byte[1]);
-
- attrView = Files.getFileAttributeView(file, DosFileAttributeView.class);
- attrView.setReadOnly(true);
- }
-
- @AfterEach
- public void tearDown() throws IOException {
- attrView.setReadOnly(false);
- }
-
- @Test
- @DisplayName("is not writable")
- public void testNotWritable() {
- Assertions.assertThrows(AccessDeniedException.class, () -> {
- FileChannel.open(file, StandardOpenOption.WRITE);
- });
- }
-
- @Test
- @DisplayName("is readable")
- public void testReadable() throws IOException {
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
- Assertions.assertEquals(1, ch.size());
- }
- }
-
- @Test
- @DisplayName("can be made read-write accessible")
- public void testFoo() throws IOException {
- attrView.setReadOnly(false);
- try (FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE)) {
- Assertions.assertEquals(1, ch.size());
- }
- }
-
- }
-
-
- }
-
-}
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the accompanying LICENSE.txt.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.cryptofs;
+
+import com.google.common.io.MoreFiles;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptofs.ch.CleartextFileChannel;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+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;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.NonReadableChannelException;
+import java.nio.channels.NonWritableChannelException;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.DosFileAttributeView;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.Set;
+
+import static java.nio.file.Files.readAllBytes;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+
+public class CryptoFileSystemProviderIntegrationTest {
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ public class WithLimitedPaths {
+
+ private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+ private CryptoFileSystem fs;
+ private Path shortFilePath;
+ private Path shortSymlinkPath;
+ private Path shortDirPath;
+
+ @BeforeAll
+ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+ Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
+ CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
+ .withFlags() //
+ .withMasterkeyFilename("masterkey.cryptomator") //
+ .withKeyLoader(keyLoader) //
+ .withMaxCleartextNameLength(50) //
+ .build();
+ CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
+ fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
+ }
+
+ @BeforeEach
+ public void setupEach() throws IOException {
+ shortFilePath = fs.getPath("/short-enough.txt");
+ shortDirPath = fs.getPath("/short-enough-dir");
+ shortSymlinkPath = fs.getPath("/symlink.txt");
+ Files.createFile(shortFilePath);
+ Files.createDirectory(shortDirPath);
+ Files.createSymbolicLink(shortSymlinkPath, shortFilePath);
+ }
+
+ @AfterEach
+ public void tearDownEach() throws IOException {
+ Files.deleteIfExists(shortFilePath);
+ Files.deleteIfExists(shortDirPath);
+ Files.deleteIfExists(shortSymlinkPath);
+ }
+
+ @DisplayName("expect create file to fail with FileNameTooLongException")
+ @Test
+ public void testCreateFileExceedingPathLengthLimit() {
+ Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
+ assertThrows(FileNameTooLongException.class, () -> {
+ Files.createFile(p);
+ });
+ }
+
+ @DisplayName("expect create directory to fail with FileNameTooLongException")
+ @Test
+ public void testCreateDirExceedingPathLengthLimit() {
+ Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
+ assertThrows(FileNameTooLongException.class, () -> {
+ Files.createDirectory(p);
+ });
+ }
+
+ @DisplayName("expect create symlink to fail with FileNameTooLongException")
+ @Test
+ public void testCreateSymlinkExceedingPathLengthLimit() {
+ Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
+ assertThrows(FileNameTooLongException.class, () -> {
+ Files.createSymbolicLink(p, shortFilePath);
+ });
+ }
+
+ @DisplayName("expect move to fail with FileNameTooLongException")
+ @ParameterizedTest(name = "move {0} -> this-cleartext-filename-is-longer-than-50-characters")
+ @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
+ public void testMoveExceedingPathLengthLimit(String path) {
+ Path src = fs.getPath(path);
+ Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
+ assertThrows(FileNameTooLongException.class, () -> {
+ Files.move(src, dst);
+ });
+ assertTrue(Files.exists(src));
+ assertTrue(Files.notExists(dst));
+ }
+
+ @DisplayName("expect copy to fail with FileNameTooLongException")
+ @ParameterizedTest(name = "copy {0} -> this-cleartext-filename-is-longer-than-50-characters")
+ @ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
+ public void testCopyExceedingPathLengthLimit(String path) {
+ Path src = fs.getPath(path);
+ Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
+ assertThrows(FileNameTooLongException.class, () -> {
+ Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS);
+ });
+ assertTrue(Files.exists(src));
+ assertTrue(Files.notExists(dst));
+ }
+
+ }
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+ public class InMemoryOrdered {
+
+ private FileSystem tmpFs;
+ private MasterkeyLoader keyLoader1;
+ private MasterkeyLoader keyLoader2;
+ private Path pathToVault1;
+ private Path pathToVault2;
+ private Path vaultConfigFile1;
+ private Path vaultConfigFile2;
+ private FileSystem fs1;
+ private FileSystem fs2;
+
+ @BeforeAll
+ public void setup() throws IOException, MasterkeyLoadingFailedException {
+ tmpFs = Jimfs.newFileSystem(Configuration.unix());
+ byte[] key1 = new byte[64];
+ byte[] key2 = new byte[64];
+ Arrays.fill(key1, (byte) 0x55);
+ Arrays.fill(key2, (byte) 0x77);
+ keyLoader1 = Mockito.mock(MasterkeyLoader.class);
+ keyLoader2 = Mockito.mock(MasterkeyLoader.class);
+ Mockito.when(keyLoader1.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key1));
+ Mockito.when(keyLoader2.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key2));
+ pathToVault1 = tmpFs.getPath("/vaultDir1");
+ pathToVault2 = tmpFs.getPath("/vaultDir2");
+ Files.createDirectory(pathToVault1);
+ Files.createDirectory(pathToVault2);
+ vaultConfigFile1 = pathToVault1.resolve("vault.cryptomator");
+ vaultConfigFile2 = pathToVault2.resolve("vault.cryptomator");
+ }
+
+ @AfterAll
+ public void teardown() throws IOException {
+ tmpFs.close();
+ }
+
+ @Test
+ @Order(1)
+ @DisplayName("initialize vaults")
+ public void initializeVaults() {
+ assertAll(() -> {
+ var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build();
+ CryptoFileSystemProvider.initialize(pathToVault1, properties, URI.create("test:key"));
+ assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
+ assertTrue(Files.isRegularFile(vaultConfigFile1));
+ }, () -> {
+ var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build();
+ CryptoFileSystemProvider.initialize(pathToVault2, properties, URI.create("test:key"));
+ assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
+ assertTrue(Files.isRegularFile(vaultConfigFile2));
+ });
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("get filesystem with incorrect credentials")
+ public void testGetFsWithWrongCredentials() throws IOException {
+ assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
+ assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
+ assertAll(() -> {
+ URI fsUri = CryptoFileSystemUri.create(pathToVault1);
+ CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
+ .withFlags() //
+ .withMasterkeyFilename("masterkey.cryptomator") //
+ .withKeyLoader(keyLoader2) //
+ .build();
+ assertThrows(VaultKeyInvalidException.class, () -> {
+ FileSystems.newFileSystem(fsUri, properties);
+ });
+ }, () -> {
+ URI fsUri = CryptoFileSystemUri.create(pathToVault2);
+ CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
+ .withFlags() //
+ .withMasterkeyFilename("masterkey.cryptomator") //
+ .withKeyLoader(keyLoader1) //
+ .build();
+ assertThrows(VaultKeyInvalidException.class, () -> {
+ FileSystems.newFileSystem(fsUri, properties);
+ });
+ });
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("get filesystem with correct credentials")
+ public void testGetFsViaNioApi() {
+ assumeTrue(Files.exists(vaultConfigFile1));
+ assumeTrue(Files.exists(vaultConfigFile2));
+ assertAll(() -> {
+ URI fsUri = CryptoFileSystemUri.create(pathToVault1);
+ fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build());
+ assertTrue(fs1 instanceof CryptoFileSystemImpl);
+
+ FileSystem sameFs = FileSystems.getFileSystem(fsUri);
+ assertSame(fs1, sameFs);
+ }, () -> {
+ URI fsUri = CryptoFileSystemUri.create(pathToVault2);
+ fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build());
+ assertTrue(fs2 instanceof CryptoFileSystemImpl);
+
+ FileSystem sameFs = FileSystems.getFileSystem(fsUri);
+ assertSame(fs2, sameFs);
+ });
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("touch /foo")
+ public void testOpenAndCloseFileChannel() throws IOException {
+ assumeTrue(fs1.isOpen());
+
+ try (FileChannel ch = FileChannel.open(fs1.getPath("/foo"), EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW))) {
+ assertTrue(ch instanceof CleartextFileChannel);
+ }
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("ln -s foo /link")
+ public void testCreateSymlink() {
+ Path target = fs1.getPath("/foo");
+ assumeTrue(Files.isRegularFile(target));
+ Path link = fs1.getPath("/link");
+
+ assertDoesNotThrow(() -> {
+ Files.createSymbolicLink(link, target);
+ });
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("echo 'hello world' > /link")
+ public void testWriteToSymlink() throws IOException {
+ Path link = fs1.getPath("/link");
+ assumeTrue(Files.isSymbolicLink(link));
+
+ assertDoesNotThrow(() -> {
+ try (WritableByteChannel ch = Files.newByteChannel(link, StandardOpenOption.WRITE)) {
+ ch.write(StandardCharsets.US_ASCII.encode("hello world"));
+ }
+ });
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("cat `readlink -f /link`")
+ public void testReadFromSymlink() throws IOException {
+ Path link = fs1.getPath("/link");
+ assumeTrue(Files.isSymbolicLink(link));
+ Path target = Files.readSymbolicLink(link);
+
+ try (ReadableByteChannel ch = Files.newByteChannel(target, StandardOpenOption.READ)) {
+ ByteBuffer buf = ByteBuffer.allocate(100);
+ ch.read(buf);
+ buf.flip();
+ String str = StandardCharsets.US_ASCII.decode(buf).toString();
+ assertEquals("hello world", str);
+ }
+ }
+
+ @Test
+ @Order(7)
+ @DisplayName("cp /link /otherlink")
+ public void testCopySymlinkSymlink() throws IOException {
+ Path src = fs1.getPath("/link");
+ Path dst = fs1.getPath("/otherlink");
+ assumeTrue(Files.isSymbolicLink(src));
+ assumeTrue(Files.notExists(dst));
+ Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS);
+ assertTrue(Files.isSymbolicLink(src));
+ assertTrue(Files.isSymbolicLink(dst));
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("rm /link")
+ public void testRemoveSymlink() throws IOException {
+ Path link = fs1.getPath("/link");
+ assumeTrue(Files.isSymbolicLink(link));
+
+ assertDoesNotThrow(() -> {
+ Files.delete(link);
+ });
+ }
+
+ @Test
+ @Order(8)
+ @DisplayName("rm /otherlink")
+ public void testRemoveOtherSymlink() throws IOException {
+ Path link = fs1.getPath("/otherlink");
+ assumeTrue(Files.isSymbolicLink(link));
+
+ assertDoesNotThrow(() -> {
+ Files.delete(link);
+ });
+ }
+
+ @Test
+ @Order(9)
+ @DisplayName("ln -s foo '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
+ public void testCreateSymlinkWithLongName() throws IOException {
+ Path target = fs1.getPath("/foo");
+ assumeTrue(Files.isRegularFile(target));
+ Path longNameLink = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ Files.createSymbolicLink(longNameLink, target);
+ MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNameLink));
+ assertTrue(Files.exists(longNameLink));
+ }
+
+ @Test
+ @Order(10)
+ @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
+ public void testMoveSymlinkWithLongNameToAnotherLongName() throws IOException {
+ Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ assumeTrue(Files.isSymbolicLink(longNameSource));
+ Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.move(longNameSource, longNameTarget);
+ assertTrue(Files.exists(longNameTarget));
+ assertTrue(Files.notExists(longNameSource));
+ }
+
+ @Test
+ @Order(11)
+ @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
+ public void testRemoveSymlinkWithLongName() throws IOException {
+ Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.delete(longNamePath);
+ assertTrue(Files.notExists(longNamePath));
+ }
+
+ @Test
+ @Order(12)
+ @DisplayName("mkdir '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
+ public void testCreateDirWithLongName() throws IOException {
+ Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ Files.createDirectory(longNamePath);
+ assertTrue(Files.isDirectory(longNamePath));
+ MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath));
+ }
+
+ @Test
+ @Order(13)
+ @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
+ public void testMoveDirWithLongNameToAnotherLongName() throws IOException {
+ Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.move(longNameSource, longNameTarget);
+ assertTrue(Files.exists(longNameTarget));
+ assertTrue(Files.notExists(longNameSource));
+ }
+
+ @Test
+ @Order(14)
+ @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
+ public void testRemoveDirWithLongName() throws IOException {
+ Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.delete(longNamePath);
+ assertTrue(Files.notExists(longNamePath));
+ }
+
+ @Test
+ @Order(15)
+ @DisplayName("touch '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet'")
+ public void testCreateFileWithLongName() throws IOException {
+ Path longNamePath = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ Files.createFile(longNamePath);
+ assertTrue(Files.isRegularFile(longNamePath));
+ MatcherAssert.assertThat(MoreFiles.listFiles(fs1.getPath("/")), Matchers.hasItem(longNamePath));
+ }
+
+ @Test
+ @Order(16)
+ @DisplayName("mv '/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet' '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat")
+ public void testMoveFileWithLongNameToAnotherLongName() throws IOException {
+ Path longNameSource = fs1.getPath("/Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet Telefon Energie Wasser Webseitengeraffel Bus Bahn Mietwagen Internet");
+ Path longNameTarget = longNameSource.resolveSibling("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.move(longNameSource, longNameTarget);
+ assertTrue(Files.exists(longNameTarget));
+ assertTrue(Files.notExists(longNameSource));
+ }
+
+ @Test
+ @Order(17)
+ @DisplayName("rm -r '/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat'")
+ public void testRemoveFileWithLongName() throws IOException {
+ Path longNamePath = fs1.getPath("/Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat Talafan Anargaa Wassar Wabsaatangaraffal Bas Bahn Maatwagan Antarnat");
+ Files.delete(longNamePath);
+ assertTrue(Files.notExists(longNamePath));
+ }
+
+ @Test
+ @Order(18)
+ @DisplayName("cp fs1:/foo fs2:/bar")
+ public void testCopyFileAcrossFilesystem() throws IOException {
+ Path file1 = fs1.getPath("/foo");
+ Path file2 = fs2.getPath("/bar");
+ assumeTrue(Files.isRegularFile(file1));
+ assumeTrue(Files.notExists(file2));
+
+ Files.copy(file1, file2);
+
+ assertArrayEquals(readAllBytes(file1), readAllBytes(file2));
+ }
+
+ @Test
+ @Order(19)
+ @DisplayName("echo 'goodbye world' > /foo")
+ public void testWriteToFile() throws IOException {
+ Path file1 = fs1.getPath("/foo");
+ assumeTrue(Files.isRegularFile(file1));
+
+ assertDoesNotThrow(() -> {
+ Files.write(file1, "goodbye world".getBytes());
+ });
+ }
+
+ @Test
+ @Order(20)
+ @DisplayName("cp -f fs1:/foo fs2:/bar")
+ public void testCopyFileAcrossFilesystemReplaceExisting() throws IOException {
+ Path file1 = fs1.getPath("/foo");
+ Path file2 = fs2.getPath("/bar");
+ assumeTrue(Files.isRegularFile(file1));
+ assumeTrue(Files.isRegularFile(file2));
+
+ Files.copy(file1, file2, REPLACE_EXISTING);
+
+ assertArrayEquals(readAllBytes(file1), readAllBytes(file2));
+ }
+
+ @Test
+ @Order(21)
+ @DisplayName("readattr /attributes.txt")
+ public void testLazinessOfFileAttributeViews() throws IOException {
+ Path file = fs1.getPath("/attributes.txt");
+ assumeTrue(Files.notExists(file));
+
+ BasicFileAttributeView attrView = Files.getFileAttributeView(file, BasicFileAttributeView.class);
+ assertNotNull(attrView);
+ assertThrows(NoSuchFileException.class, () -> {
+ attrView.readAttributes();
+ });
+
+ Files.write(file, new byte[3], StandardOpenOption.CREATE_NEW);
+ BasicFileAttributes attrs = attrView.readAttributes();
+ assertNotNull(attrs);
+ assertEquals(3, attrs.size());
+
+ Files.delete(file);
+ assertThrows(NoSuchFileException.class, () -> {
+ attrView.readAttributes();
+ });
+ assertEquals(3, attrs.size()); // attrs should be immutable once they are read.
+ }
+
+ @Test
+ @Order(22)
+ @DisplayName("ln -s /linked/targetY /links/linkX")
+ public void testSymbolicLinks() throws IOException {
+ Path linksDir = fs1.getPath("/links");
+ assumeTrue(Files.notExists(linksDir));
+ Files.createDirectories(linksDir);
+
+ assertAll(() -> {
+ Path link = linksDir.resolve("link1");
+ Files.createDirectories(link.getParent());
+ Files.createSymbolicLink(link, fs1.getPath("/linked/target1"));
+ Path target = Files.readSymbolicLink(link);
+ MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem())); // as per contract of readSymbolicLink
+ MatcherAssert.assertThat(target.toString(), Matchers.equalTo("/linked/target1"));
+ MatcherAssert.assertThat(link.resolveSibling(target).toString(), Matchers.equalTo("/linked/target1"));
+ }, () -> {
+ Path link = linksDir.resolve("link2");
+ Files.createDirectories(link.getParent());
+ Files.createSymbolicLink(link, fs1.getPath("./target2"));
+ Path target = Files.readSymbolicLink(link);
+ MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem()));
+ MatcherAssert.assertThat(target.toString(), Matchers.equalTo("./target2"));
+ MatcherAssert.assertThat(link.resolveSibling(target).normalize().toString(), Matchers.equalTo("/links/target2"));
+ }, () -> {
+ Path link = linksDir.resolve("link3");
+ Files.createDirectories(link.getParent());
+ Files.createSymbolicLink(link, fs1.getPath("../target3"));
+ Path target = Files.readSymbolicLink(link);
+ MatcherAssert.assertThat(target.getFileSystem(), is(link.getFileSystem()));
+ MatcherAssert.assertThat(target.toString(), Matchers.equalTo("../target3"));
+ MatcherAssert.assertThat(link.resolveSibling(target).normalize().toString(), Matchers.equalTo("/target3"));
+ });
+ }
+
+ @Test
+ @Order(22)
+ @DisplayName("mv -f fs1:/foo fs2:/baz")
+ public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException {
+ Path file1 = fs1.getPath("/foo");
+ Path file2 = fs2.getPath("/baz");
+ assumeTrue(Files.isRegularFile(file1));
+ assumeTrue(Files.notExists(file2));
+ byte[] contents = readAllBytes(file1);
+
+ Files.move(file1, file2);
+
+ assertTrue(Files.notExists(file1));
+ assertTrue(Files.isRegularFile(file2));
+ assertArrayEquals(contents, readAllBytes(file2));
+ }
+
+ }
+
+ @Nested
+ @EnabledOnOs({OS.MAC, OS.LINUX})
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ @DisplayName("On POSIX Systems")
+ public class PosixTests {
+
+ private FileSystem fs;
+
+ @BeforeAll
+ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+ Path pathToVault = tmpDir.resolve("vaultDir1");
+ Files.createDirectories(pathToVault);
+ MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+ Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
+ var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+ CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
+ fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
+ }
+
+ @Nested
+ @DisplayName("File Locks")
+ public class FileLockTests {
+
+ private Path file = fs.getPath("/lock.txt");
+
+ @BeforeEach
+ public void setup() throws IOException {
+ Files.write(file, new byte[100000]); // > 3 * 32k
+ }
+
+ @Test
+ @DisplayName("get shared lock on non-readable channel fails")
+ public void testGetSharedLockOnNonReadableChannel() throws IOException {
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE)) {
+ assertThrows(NonReadableChannelException.class, () -> {
+ ch.lock(0, 50000, true);
+ });
+ }
+ }
+
+ @Test
+ @DisplayName("locking a closed channel fails")
+ public void testLockClosedChannel() throws IOException {
+ FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE);
+ ch.close();
+ assertThrows(ClosedChannelException.class, () -> {
+ ch.lock();
+ });
+ }
+
+ @Test
+ @DisplayName("get exclusive lock on non-writable channel fails")
+ public void testGetSharedLockOnNonWritableChannel() throws IOException {
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
+ assertThrows(NonWritableChannelException.class, () -> {
+ ch.lock(0, 50000, false);
+ });
+ }
+ }
+
+ @ParameterizedTest(name = "shared = {0}")
+ @CsvSource({"true", "false"})
+ @DisplayName("create non-overlapping locks")
+ public void testNonOverlappingLocks(boolean shared) throws IOException {
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
+ try (FileLock lock1 = ch.lock(0, 10000, shared)) {
+ try (FileLock lock2 = ch.lock(90000, 10000, shared)) {
+ assertNotSame(lock1, lock2);
+ }
+ }
+ }
+ }
+
+ @ParameterizedTest(name = "shared = {0}")
+ @CsvSource({"true", "false"})
+ @DisplayName("create overlapping locks")
+ public void testOverlappingLocks(boolean shared) throws IOException {
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
+ try (FileLock lock1 = ch.lock(0, 10000, shared)) {
+ // while bock locks cover different cleartext byte ranges, it is necessary to lock the same ciphertext block
+ assertThrows(OverlappingFileLockException.class, () -> {
+ ch.lock(10000, 10000, shared);
+ });
+ }
+ }
+ }
+
+ }
+
+
+ }
+
+ @Nested
+ @EnabledOnOs(OS.WINDOWS)
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ @DisplayName("On Windows Systems")
+ public class WindowsTests {
+
+ private FileSystem fs;
+
+ @BeforeAll
+ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+ Path pathToVault = tmpDir.resolve("vaultDir1");
+ Files.createDirectories(pathToVault);
+ MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+ Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
+ var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+ CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
+ fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
+ }
+
+ @Test
+ @DisplayName("set dos attributes")
+ public void testDosFileAttributes() throws IOException {
+ Path file = fs.getPath("/msDosAttributes.txt");
+ assumeTrue(Files.notExists(file));
+
+ Files.write(file, new byte[1]);
+
+ Files.setAttribute(file, "dos:hidden", true);
+ Files.setAttribute(file, "dos:system", true);
+ Files.setAttribute(file, "dos:archive", true);
+ Files.setAttribute(file, "dos:readOnly", true);
+
+ assertEquals(true, Files.getAttribute(file, "dos:hidden"));
+ assertEquals(true, Files.getAttribute(file, "dos:system"));
+ assertEquals(true, Files.getAttribute(file, "dos:archive"));
+ assertEquals(true, Files.getAttribute(file, "dos:readOnly"));
+
+ Files.setAttribute(file, "dos:hidden", false);
+ Files.setAttribute(file, "dos:system", false);
+ Files.setAttribute(file, "dos:archive", false);
+ Files.setAttribute(file, "dos:readOnly", false);
+
+ assertEquals(false, Files.getAttribute(file, "dos:hidden"));
+ assertEquals(false, Files.getAttribute(file, "dos:system"));
+ assertEquals(false, Files.getAttribute(file, "dos:archive"));
+ assertEquals(false, Files.getAttribute(file, "dos:readOnly"));
+ }
+
+ @Nested
+ @DisplayName("read-only file")
+ public class OnReadOnlyFile {
+
+ private Path file = fs.getPath("/readonly.txt");
+ private DosFileAttributeView attrView;
+
+ @BeforeEach
+ public void setup() throws IOException {
+ Files.write(file, new byte[1]);
+
+ attrView = Files.getFileAttributeView(file, DosFileAttributeView.class);
+ attrView.setReadOnly(true);
+ }
+
+ @AfterEach
+ public void tearDown() throws IOException {
+ attrView.setReadOnly(false);
+ }
+
+ @Test
+ @DisplayName("is not writable")
+ public void testNotWritable() {
+ assertThrows(AccessDeniedException.class, () -> {
+ FileChannel.open(file, StandardOpenOption.WRITE);
+ });
+ }
+
+ @Test
+ @DisplayName("is readable")
+ public void testReadable() throws IOException {
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
+ assertEquals(1, ch.size());
+ }
+ }
+
+ @Test
+ @DisplayName("can be made read-write accessible")
+ public void testFoo() throws IOException {
+ attrView.setReadOnly(false);
+ try (FileChannel ch = FileChannel.open(file, StandardOpenOption.WRITE)) {
+ assertEquals(1, ch.size());
+ }
+ }
+
+ }
+
+
+ }
+
+}