diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java index 82563853..3b89dabe 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java @@ -13,30 +13,26 @@ import org.cryptomator.cryptofs.Symlinks; import org.cryptomator.cryptofs.common.ArrayUtils; import org.cryptomator.cryptofs.common.CiphertextFileType; -import org.cryptomator.cryptofs.fh.OpenCryptoFile; -import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import java.io.IOException; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.FileAttributeView; -import java.util.Optional; abstract sealed class AbstractCryptoFileAttributeView implements FileAttributeView - permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView { + permits CryptoBasicFileAttributeView, CryptoFileOwnerAttributeView, CryptoUserDefinedFileAttributeView { protected final CryptoPath cleartextPath; private final CryptoPathMapper pathMapper; protected final LinkOption[] linkOptions; private final Symlinks symlinks; - private final OpenCryptoFiles openCryptoFiles; - protected AbstractCryptoFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles) { + + protected AbstractCryptoFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks) { this.cleartextPath = cleartextPath; this.pathMapper = pathMapper; this.linkOptions = linkOptions; this.symlinks = symlinks; - this.openCryptoFiles = openCryptoFiles; } protected T getCiphertextAttributeView(Class delegateType) throws IOException { @@ -44,12 +40,7 @@ protected T getCiphertextAttributeView(Class de return ciphertextPath.getFileSystem().provider().getFileAttributeView(ciphertextPath, delegateType); } - protected Optional getOpenCryptoFile() throws IOException { - Path ciphertextPath = getCiphertextPath(cleartextPath); - return openCryptoFiles.get(ciphertextPath); - } - - private Path getCiphertextPath(CryptoPath path) throws IOException { + protected Path getCiphertextPath(CryptoPath path) throws IOException { CiphertextFileType type = pathMapper.getCiphertextFileType(path); return switch (type) { case SYMLINK: diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java index d41836c1..b9ecc41d 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewModule.java @@ -12,6 +12,7 @@ import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.UserDefinedFileAttributeView; import java.util.Map; import java.util.Optional; @@ -42,6 +43,12 @@ abstract class AttributeViewModule { @AttributeViewScoped public abstract FileAttributeView provideFileOwnerAttributeView(CryptoFileOwnerAttributeView view); + @Binds + @IntoMap + @ClassKey(UserDefinedFileAttributeView.class) + @AttributeViewScoped + public abstract FileAttributeView provideUserDefinedAttributeView(CryptoUserDefinedFileAttributeView view); + @Provides @AttributeViewScoped public static Optional provideAttributeView(Map, Provider> providers, Class requestedType) { diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java index 7d7007b2..d3809cb3 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeViewType.java @@ -5,6 +5,7 @@ import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.UserDefinedFileAttributeView; import java.util.Arrays; import java.util.Optional; @@ -12,7 +13,8 @@ public enum AttributeViewType { BASIC(BasicFileAttributeView.class, "basic"), OWNER(FileOwnerAttributeView.class, "owner"), POSIX(PosixFileAttributeView.class, "posix"), - DOS(DosFileAttributeView.class, "dos"); + DOS(DosFileAttributeView.class, "dos"), + USER(UserDefinedFileAttributeView.class, "user"); private final Class type; private final String viewName; diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java index 1e44bfa1..1a5337d6 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributeView.java @@ -12,25 +12,30 @@ import org.cryptomator.cryptofs.CryptoPathMapper; import org.cryptomator.cryptofs.ReadonlyFlag; import org.cryptomator.cryptofs.Symlinks; +import org.cryptomator.cryptofs.fh.OpenCryptoFile; import org.cryptomator.cryptofs.fh.OpenCryptoFiles; import javax.inject.Inject; import java.io.IOException; import java.nio.file.LinkOption; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.util.Optional; @AttributeViewScoped -sealed class CryptoBasicFileAttributeView extends AbstractCryptoFileAttributeView implements BasicFileAttributeView - permits CryptoDosFileAttributeView, CryptoPosixFileAttributeView { +sealed class CryptoBasicFileAttributeView extends AbstractCryptoFileAttributeView implements BasicFileAttributeView permits CryptoDosFileAttributeView, CryptoPosixFileAttributeView { + private final OpenCryptoFiles openCryptoFiles; protected final AttributeProvider fileAttributeProvider; protected final ReadonlyFlag readonlyFlag; + @Inject public CryptoBasicFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles, AttributeProvider fileAttributeProvider, ReadonlyFlag readonlyFlag) { - super(cleartextPath, pathMapper, linkOptions, symlinks, openCryptoFiles); + super(cleartextPath, pathMapper, linkOptions, symlinks); + this.openCryptoFiles = openCryptoFiles; this.fileAttributeProvider = fileAttributeProvider; this.readonlyFlag = readonlyFlag; } @@ -45,6 +50,11 @@ public BasicFileAttributes readAttributes() throws IOException { return fileAttributeProvider.readAttributes(cleartextPath, BasicFileAttributes.class, linkOptions); } + private Optional getOpenCryptoFile() throws IOException { + Path ciphertextPath = getCiphertextPath(cleartextPath); + return openCryptoFiles.get(ciphertextPath); + } + @Override public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTime createTime) throws IOException { readonlyFlag.assertWritable(); @@ -53,5 +63,4 @@ public void setTimes(FileTime lastModifiedTime, FileTime lastAccessTime, FileTim getOpenCryptoFile().ifPresent(file -> file.setLastModifiedTime(lastModifiedTime)); } } - } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java index 42678036..21677180 100644 --- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeView.java @@ -26,8 +26,8 @@ final class CryptoFileOwnerAttributeView extends AbstractCryptoFileAttributeView private final ReadonlyFlag readonlyFlag; @Inject - public CryptoFileOwnerAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, OpenCryptoFiles openCryptoFiles, ReadonlyFlag readonlyFlag) { - super(cleartextPath, pathMapper, linkOptions, symlinks, openCryptoFiles); + public CryptoFileOwnerAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, ReadonlyFlag readonlyFlag) { + super(cleartextPath, pathMapper, linkOptions, symlinks); this.readonlyFlag = readonlyFlag; } diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java new file mode 100644 index 00000000..908ea2c6 --- /dev/null +++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeView.java @@ -0,0 +1,125 @@ +package org.cryptomator.cryptofs.attr; + +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.Symlinks; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel; +import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel; + +import javax.inject.Inject; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.file.LinkOption; +import java.nio.file.attribute.UserDefinedFileAttributeView; +import java.util.List; + +@AttributeViewScoped +final class CryptoUserDefinedFileAttributeView extends AbstractCryptoFileAttributeView implements UserDefinedFileAttributeView { + + private static final String PREFIX = "c9r."; + + private final Cryptor cryptor; + + @Inject + public CryptoUserDefinedFileAttributeView(CryptoPath cleartextPath, CryptoPathMapper pathMapper, LinkOption[] linkOptions, Symlinks symlinks, Cryptor cryptor) { + super(cleartextPath, pathMapper, linkOptions, symlinks); + this.cryptor = cryptor; + } + + @Override + public String name() { + return "user"; // as per contract + } + + @Override + public List list() throws IOException { + var ciphertextNames = getCiphertextAttributeView(UserDefinedFileAttributeView.class).list(); + return ciphertextNames.stream().filter(s -> s.startsWith(PREFIX)).map(this::decryptName).toList(); + } + + @Override + public int size(String cleartextName) throws IOException { + var ciphertextName = encryptName(cleartextName); + var totalCiphertextSize = getCiphertextAttributeView(UserDefinedFileAttributeView.class).size(ciphertextName); + var ciphertextBodySize = totalCiphertextSize - cryptor.fileHeaderCryptor().headerSize(); + return (int) cryptor.fileContentCryptor().cleartextSize(ciphertextBodySize); + } + + @Override + public int read(String cleartextName, ByteBuffer dst) throws IOException { + var ciphertextName = encryptName(cleartextName); + var view = getCiphertextAttributeView(UserDefinedFileAttributeView.class); + int size = view.size(ciphertextName); + var buf = ByteBuffer.allocate(size); + view.read(ciphertextName, buf); + buf.flip(); + + try (var in = new ByteBufferInputStream(buf); // + var ciphertextChannel = Channels.newChannel(in); // + var cleartextChannel = new DecryptingReadableByteChannel(ciphertextChannel, cryptor, true)) { + return cleartextChannel.read(dst); + } + } + + @Override + public int write(String cleartextName, ByteBuffer src) throws IOException { + var ciphertextName = encryptName(cleartextName); + var out = new ByteArrayOutputStream(); + final int size; + try (var ciphertextChannel = Channels.newChannel(out); // + var cleartextChannel = new EncryptingWritableByteChannel(ciphertextChannel, cryptor)) { + size = cleartextChannel.write(src); + } // close to flush cached ciphertext + var buf = ByteBuffer.wrap(out.toByteArray()); + getCiphertextAttributeView(UserDefinedFileAttributeView.class).write(ciphertextName, buf); + return size; + } + + @Override + public void delete(String cleartextName) throws IOException { + var ciphertextName = encryptName(cleartextName); + getCiphertextAttributeView(UserDefinedFileAttributeView.class).delete(ciphertextName); + } + + private String encryptName(String cleartextName) { + return PREFIX + cryptor.fileNameCryptor().encryptFilename(BaseEncoding.base64Url(), cleartextName); + } + + private String decryptName(String ciphertextName) { + assert ciphertextName.startsWith(PREFIX); + return cryptor.fileNameCryptor().decryptFilename(BaseEncoding.base64Url(), ciphertextName.substring(PREFIX.length())); + } + + // taken from https://stackoverflow.com/a/6603018/4014509 + private static class ByteBufferInputStream extends InputStream { + + ByteBuffer buf; + + public ByteBufferInputStream(ByteBuffer buf) { + this.buf = buf; + } + + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + public int read(byte[] bytes, int off, int len) throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java index 1ad616ad..5ab35376 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoFileOwnerAttributeViewTest.java @@ -39,7 +39,6 @@ public class CryptoFileOwnerAttributeViewTest { private CryptoPath cleartextPath = mock(CryptoPath.class); private CryptoPathMapper pathMapper = mock(CryptoPathMapper.class); private Symlinks symlinks = mock(Symlinks.class); - private OpenCryptoFiles openCryptoFiles = mock(OpenCryptoFiles.class); private ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class); private CryptoFileOwnerAttributeView inTest; @@ -61,7 +60,7 @@ public void setup() throws IOException { when(linkCiphertextPath.getSymlinkFilePath()).thenReturn(linkCiphertextRawPath); when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); - inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag); + inTest = new CryptoFileOwnerAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, readonlyFlag); } @Test @@ -91,7 +90,7 @@ public void testSetOwnerDelegates() throws IOException { public void testSetOwnerOfSymlinkNoFollow() throws IOException { UserPrincipal principal = mock(UserPrincipal.class); - CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}, symlinks, openCryptoFiles, readonlyFlag); + CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{LinkOption.NOFOLLOW_LINKS}, symlinks, readonlyFlag); view.setOwner(principal); verify(linkDelegate).setOwner(principal); @@ -101,7 +100,7 @@ public void testSetOwnerOfSymlinkNoFollow() throws IOException { public void testSetOwnerOfSymlinkFollow() throws IOException { UserPrincipal principal = mock(UserPrincipal.class); - CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{}, symlinks, openCryptoFiles, readonlyFlag); + CryptoFileOwnerAttributeView view = new CryptoFileOwnerAttributeView(link, pathMapper, new LinkOption[]{}, symlinks, readonlyFlag); view.setOwner(principal); verify(delegate).setOwner(principal); diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java new file mode 100644 index 00000000..a3f65c6e --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoUserDefinedFileAttributeViewTest.java @@ -0,0 +1,134 @@ +package org.cryptomator.cryptofs.attr; + +import org.cryptomator.cryptofs.CiphertextFilePath; +import org.cryptomator.cryptofs.CryptoPath; +import org.cryptomator.cryptofs.CryptoPathMapper; +import org.cryptomator.cryptofs.Symlinks; +import org.cryptomator.cryptofs.common.CiphertextFileType; +import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileContentCryptor; +import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.UserDefinedFileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CryptoUserDefinedFileAttributeViewTest { + + private final Cryptor cryptor = mock(Cryptor.class); + private final FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class); + private final FileHeaderCryptor fileHeaderCryptor = mock(FileHeaderCryptor.class); + private final FileContentCryptor fileContentCryptor = mock(FileContentCryptor.class); + private final CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class); + private final Path ciphertextRawPath = mock(Path.class); + private final FileSystem fileSystem = mock(FileSystem.class); + private final FileSystemProvider provider = mock(FileSystemProvider.class); + private final UserDefinedFileAttributeView delegate = mock(UserDefinedFileAttributeView.class); + private final CryptoPath cleartextPath = mock(CryptoPath.class); + private final CryptoPathMapper pathMapper = mock(CryptoPathMapper.class); + private final Symlinks symlinks = mock(Symlinks.class); + + private CryptoUserDefinedFileAttributeView inTest; + + @BeforeEach + public void setUp() throws IOException { + when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor); + when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); + when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); + when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(1)); + when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(1)); + when(fileHeaderCryptor.headerSize()).thenReturn(4); + when(fileHeaderCryptor.decryptHeader(Mockito.any())).thenReturn(Mockito.mock(FileHeader.class)); + when(fileHeaderCryptor.encryptHeader(Mockito.any())).thenReturn(StandardCharsets.UTF_8.encode("HEAD")); + when(fileContentCryptor.ciphertextChunkSize()).thenReturn(1024); + when(fileContentCryptor.cleartextChunkSize()).thenReturn(1024); + when(fileContentCryptor.cleartextSize(Mockito.anyLong())).thenCallRealMethod(); + when(fileContentCryptor.decryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any(), Mockito.anyBoolean())).thenAnswer(invocation -> invocation.getArgument(0)); + when(fileContentCryptor.encryptChunk(Mockito.any(), Mockito.anyLong(), Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); + + when(ciphertextRawPath.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.provider()).thenReturn(provider); + when(provider.getFileAttributeView(ciphertextRawPath, UserDefinedFileAttributeView.class)).thenReturn(delegate); + when(pathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE); + when(pathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath); + when(ciphertextPath.getFilePath()).thenReturn(ciphertextRawPath); + + inTest = new CryptoUserDefinedFileAttributeView(cleartextPath, pathMapper, new LinkOption[]{}, symlinks, cryptor); + } + + @Test + public void testName() { + Assertions.assertEquals("user", inTest.name()); + } + + @Test + public void testList() throws IOException { + when(delegate.list()).thenReturn(Arrays.asList("c9r.Foo", "c9r.Bar")); + List result = inTest.list(); + MatcherAssert.assertThat(result, CoreMatchers.hasItems("Foo", "Bar")); + } + + @Test + public void testSize() throws IOException { + when(delegate.size("c9r.Foo")).thenReturn(50); + int result = inTest.size("Foo"); + Assertions.assertEquals(46, result); + } + + @Test + public void testWrite() throws IOException { + when(delegate.write(Mockito.eq("c9r.Foo"), Mockito.any())).thenAnswer(invocation -> { + ByteBuffer buf = invocation.getArgument(1); + int len = buf.remaining(); + buf.position(buf.position() + len); + return len; + }); + + int written = inTest.write("Foo", StandardCharsets.UTF_8.encode("Hello World")); + Assertions.assertEquals("Hello World".length(), written); + Mockito.verify(delegate).write(Mockito.eq("c9r.Foo"), Mockito.any()); + } + + @Test + public void testRead() throws IOException { + byte[] content = "HEADHello World".getBytes(StandardCharsets.UTF_8); + when(delegate.size("c9r.Foo")).thenReturn(content.length); + when(delegate.read(Mockito.eq("c9r.Foo"), Mockito.any())).thenAnswer(invocation -> { + ByteBuffer buf = invocation.getArgument(1); + buf.put(content); + return content.length; + }); + + ByteBuffer buf = ByteBuffer.allocate(inTest.size("Foo")); + int read = inTest.read("Foo", buf); + Assertions.assertEquals("Hello World".length(), read); + buf.flip(); + Assertions.assertEquals("Hello World", StandardCharsets.UTF_8.decode(buf).toString()); + } + + @Test + public void testDelete() throws IOException { + inTest.delete("Foo"); + Mockito.verify(delegate).delete(Mockito.eq("c9r.Foo")); + + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java index 467b5598..1bf0cdb8 100644 --- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java @@ -8,7 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptofs.attr; +import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; +import com.google.common.jimfs.PathType; import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptolib.api.Masterkey; @@ -22,13 +24,22 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; +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.params.ParameterizedTest; +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.FileChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -37,6 +48,7 @@ import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.UserDefinedFileAttributeView; import java.nio.file.attribute.UserPrincipal; import java.time.Instant; import java.util.Map; @@ -58,7 +70,7 @@ public class FileAttributeIntegrationTest { @BeforeAll public static void setupClass() throws IOException, MasterkeyLoadingFailedException { - inMemoryFs = Jimfs.newFileSystem(); + inMemoryFs = Jimfs.newFileSystem(Configuration.unix().toBuilder().setAttributeViews("basic", "owner", "user").build()); pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault"); Files.createDirectory(pathToVault); MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class); @@ -193,6 +205,78 @@ public void testFileAttributeViewUpdatesAfterMove() throws IOException { } } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @DisplayName("Extended Attributes") + public class UserDefinedFileAttributes { + + private Path file; + + @BeforeAll + public void setup() throws IOException { + Assumptions.assumeTrue(inMemoryFs.supportedFileAttributeViews().contains("user")); + Assumptions.assumeTrue(fileSystem.supportedFileAttributeViews().contains("user")); + file = fileSystem.getPath("/xattr.txt"); + Files.createFile(file); + } + + @Order(1) + @DisplayName("setxattr /xattr.txt") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"attr1", "attr2", "attr3", "attr4", "attr5"}) + public void testSetxattr(String attrName) throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var attrValue = StandardCharsets.UTF_8.encode(attrName); + + int written = attrView.write(attrName, attrValue); + + Assertions.assertEquals(attrName.length(), written); + } + + @Order(2) + @Test + @DisplayName("removexattr /xattr.txt") + public void testRemovexattr() { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + + Assertions.assertDoesNotThrow(() -> attrView.delete("attr3")); + } + + @Order(3) + @Test + @DisplayName("listxattr /xattr.txt") + public void testListxattr() throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var result = attrView.list(); + + Assertions.assertAll( + () -> Assertions.assertTrue(result.contains("attr1")), + () -> Assertions.assertTrue(result.contains("attr2")), + () -> Assertions.assertFalse(result.contains("attr3")), + () -> Assertions.assertTrue(result.contains("attr4")), + () -> Assertions.assertTrue(result.contains("attr5")) + ); + } + + @Order(4) + @DisplayName("getxattr") + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"attr1", "attr2", "attr4", "attr5"}) + public void testGetxattr(String attrName) throws IOException { + var attrView = Files.getFileAttributeView(file, UserDefinedFileAttributeView.class); + var buffer = ByteBuffer.allocate(attrView.size(attrName)); + var read = attrView.read(attrName, buffer); + buffer.flip(); + var value = StandardCharsets.UTF_8.decode(buffer).toString(); + + Assertions.assertEquals(attrName.length(), read); + Assertions.assertEquals(attrName, value); + } + + } + + private static Matcher isAfter(FileTime previousFileTime) { return new BaseMatcher<>() { @Override