diff --git a/main/filesystem-api/src/main/java/org/cryptomator/io/ByteBuffers.java b/main/filesystem-api/src/main/java/org/cryptomator/io/ByteBuffers.java index 37597fb6c..7b6de50d1 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/io/ByteBuffers.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/io/ByteBuffers.java @@ -10,9 +10,6 @@ package org.cryptomator.io; import java.nio.ByteBuffer; -/** - * TODO this probably doesn't belong into this maven module, but it is used by various filesystem layers. - */ public final class ByteBuffers { private ByteBuffers() { diff --git a/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java new file mode 100644 index 000000000..6152f65cb --- /dev/null +++ b/main/filesystem-api/src/main/java/org/cryptomator/io/FileContents.java @@ -0,0 +1,56 @@ +package org.cryptomator.io; + +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.channels.Channels; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.io.IOUtils; +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.WritableFile; + +public final class FileContents { + + public static final FileContents UTF_8 = FileContents.withCharset(StandardCharsets.UTF_8); + + private final Charset charset; + + private FileContents(Charset charset) { + this.charset = charset; + } + + /** + * Reads the whole content from the given file. + * + * @param file File whose content should be read. + * @return The file's content interpreted in this FileContents' charset. + */ + public String readContents(File file) { + try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) { + return IOUtils.toString(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Writes the string into the file encoded with this FileContents' charset. + * This methods replaces any previously existing content, i.e. the string will be the sole content. + * + * @param file File whose content should be written. + * @param content The new content. + */ + public void writeContents(File file, String content) { + try (WritableFile writable = file.openWritable()) { + writable.truncate(); + writable.write(charset.encode(content)); + } + } + + public static FileContents withCharset(Charset charset) { + return new FileContents(charset); + } + +} diff --git a/main/filesystem-api/src/test/java/org/cryptomator/io/FileContentsTest.java b/main/filesystem-api/src/test/java/org/cryptomator/io/FileContentsTest.java new file mode 100644 index 000000000..464a2d726 --- /dev/null +++ b/main/filesystem-api/src/test/java/org/cryptomator/io/FileContentsTest.java @@ -0,0 +1,103 @@ +package org.cryptomator.io; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.ReadableFile; +import org.cryptomator.filesystem.WritableFile; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +@RunWith(Theories.class) +public class FileContentsTest { + + @DataPoints + public static final Iterable CHARSETS = Arrays.asList(StandardCharsets.UTF_8, StandardCharsets.US_ASCII, StandardCharsets.UTF_16); + + @DataPoints + public static final Iterable TEST_CONTENTS = Arrays.asList("hello world", "hellö wörld", ""); + + @Theory + public void testReadAll(Charset charset, String testString) { + Assume.assumeTrue(charset.newEncoder().canEncode(testString)); + + ByteBuffer testContent = ByteBuffer.wrap(testString.getBytes(charset)); + File file = Mockito.mock(File.class); + ReadableFile readable = Mockito.mock(ReadableFile.class); + Mockito.when(file.openReadable()).thenReturn(readable); + Mockito.when(readable.read(Mockito.any(ByteBuffer.class))).then(invocation -> { + ByteBuffer target = invocation.getArgumentAt(0, ByteBuffer.class); + if (testContent.hasRemaining()) { + return ByteBuffers.copy(testContent, target); + } else { + return -1; + } + }); + + String contentsRead = FileContents.withCharset(charset).readContents(file); + Assert.assertEquals(testString, contentsRead); + } + + @Theory + public void testWriteAll(Charset charset, String testString) { + Assume.assumeTrue(charset.newEncoder().canEncode(testString)); + + ByteBuffer testContent = ByteBuffer.allocate(100); + File file = Mockito.mock(File.class); + WritableFile writable = Mockito.mock(WritableFile.class); + Mockito.when(file.openWritable()).thenReturn(writable); + Mockito.doAnswer(invocation -> { + testContent.clear(); + return null; + }).when(writable).truncate(); + Mockito.when(writable.write(Mockito.any(ByteBuffer.class))).then(invocation -> { + ByteBuffer source = invocation.getArgumentAt(0, ByteBuffer.class); + if (testContent.hasRemaining()) { + return ByteBuffers.copy(source, testContent); + } else { + return -1; + } + }); + + FileContents.withCharset(charset).writeContents(file, testString); + Assert.assertArrayEquals(testString.getBytes(charset), Arrays.copyOf(testContent.array(), testContent.position())); + } + + @Test(expected = UncheckedIOException.class) + public void testIOExceptionDuringRead() { + File file = Mockito.mock(File.class); + Mockito.when(file.openReadable()).thenAnswer(invocation -> { + throw new IOException("failed"); + }); + + FileContents.UTF_8.readContents(file); + } + + @Test(expected = UncheckedIOException.class) + public void testUncheckedIOExceptionDuringRead() { + File file = Mockito.mock(File.class); + Mockito.when(file.openReadable()).thenThrow(new UncheckedIOException(new IOException("failed"))); + + FileContents.UTF_8.readContents(file); + } + + @Test(expected = UncheckedIOException.class) + public void testUncheckedIOExceptionDuringWrite() { + File file = Mockito.mock(File.class); + Mockito.when(file.openWritable()).thenThrow(new UncheckedIOException(new IOException("failed"))); + + FileContents.UTF_8.writeContents(file, "hello world"); + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index 67889125c..21ef76d80 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -71,21 +71,9 @@ class CryptoFolder extends CryptoNode implements Folder { } protected Optional getDirectoryId() { - if (directoryId.get() != null) { - return Optional.of(directoryId.get()); - } - if (physicalFile().isPresent()) { - File dirFile = physicalFile().get(); - if (dirFile.exists()) { - try (Reader reader = Channels.newReader(dirFile.openReadable(), UTF_8.newDecoder(), -1)) { - directoryId.set(IOUtils.toString(reader)); - return Optional.of(directoryId.get()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - } - return Optional.empty(); + return Optional.ofNullable(LazyInitializer.initializeLazily(directoryId, () -> { + return physicalFile().filter(File::exists).map(FileContents.UTF_8::readContents).orElse(null); + })); } @Override @@ -156,11 +144,7 @@ class CryptoFolder extends CryptoNode implements Folder { if (parent.file(name).exists()) { throw new UncheckedIOException(new FileAlreadyExistsException(toString())); } - try (Writer writer = Channels.newWriter(dirFile.openWritable(), UTF_8.newEncoder(), -1)) { - writer.write(directoryId.get()); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + FileContents.UTF_8.writeContents(dirFile, directoryId.get()); dir.create(); } diff --git a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java index 51f3e2be1..121e8c267 100644 --- a/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java +++ b/main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java @@ -8,22 +8,16 @@ *******************************************************************************/ package org.cryptomator.filesystem.shortening; -import static java.nio.charset.StandardCharsets.UTF_8; - import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Reader; import java.io.UncheckedIOException; -import java.io.Writer; -import java.nio.channels.Channels; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.BaseNCodec; -import org.apache.commons.io.IOUtils; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; +import org.cryptomator.io.FileContents; class FilenameShortener { @@ -64,11 +58,7 @@ class FilenameShortener { final File mappingFile = mappingFile(shortName); if (!mappingFile.exists()) { mappingFile.parent().get().create(); - try (Writer writer = Channels.newWriter(mappingFile.openWritable(), UTF_8.newEncoder(), -1)) { - writer.write(longName); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + FileContents.UTF_8.writeContents(mappingFile, longName); } } @@ -82,11 +72,7 @@ class FilenameShortener { if (!mappingFile.exists()) { throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile)); } else { - try (Reader reader = Channels.newReader(mappingFile.openReadable(), UTF_8.newDecoder(), -1)) { - return IOUtils.toString(reader); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + return FileContents.UTF_8.readContents(mappingFile); } }