mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-20 11:41:26 +00:00
Merge branch 'feature/cryptofs' into develop
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
<!-- copy resources to target/: -->
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
<version>3.0.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
@@ -79,8 +79,8 @@
|
||||
|
||||
<!-- create antkit.tar.gz: -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.cryptomator.common.test;
|
||||
|
||||
import static java.nio.file.Files.walkFileTree;
|
||||
import static java.util.Collections.synchronizedSet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.FileVisitor;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TempFilesRemovedOnShutdown {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TempFilesRemovedOnShutdown.class);
|
||||
|
||||
private static final Set<Path> PATHS_TO_REMOVE_ON_SHUTDOWN = synchronizedSet(new HashSet<>());
|
||||
private static final Thread ON_SHUTDOWN_DELETER = new Thread(TempFilesRemovedOnShutdown::removeAll);
|
||||
|
||||
static {
|
||||
Runtime.getRuntime().addShutdownHook(ON_SHUTDOWN_DELETER);
|
||||
}
|
||||
|
||||
public static Path createTempDirectory(String prefix) throws IOException {
|
||||
Path path = Files.createTempDirectory(prefix);
|
||||
PATHS_TO_REMOVE_ON_SHUTDOWN.add(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void removeAll() {
|
||||
PATHS_TO_REMOVE_ON_SHUTDOWN.forEach(TempFilesRemovedOnShutdown::remove);
|
||||
}
|
||||
|
||||
private static void remove(Path path) {
|
||||
try {
|
||||
tryRemove(path);
|
||||
} catch (Throwable e) {
|
||||
LOG.debug("Failed to remove " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryRemove(Path path) throws IOException {
|
||||
walkFileTree(path, new FileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.cryptomator.common.test.matcher;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.Matchers;
|
||||
|
||||
/**
|
||||
* Wraps hamcrest contains and containsInAnyOrder matcher factory methods to
|
||||
* avoid problems due to incorrect / inconsistent handling of generics by
|
||||
* several java compilers.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public class ContainsMatcher {
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@SafeVarargs
|
||||
public static <T> Matcher<Iterable<? super T>> containsInAnyOrder(Matcher<? extends T>... matchers) {
|
||||
return Matchers.containsInAnyOrder((Matcher[]) matchers);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@SafeVarargs
|
||||
public static <T> Matcher<Iterable<? super T>> contains(Matcher<? extends T>... matchers) {
|
||||
return Matchers.contains((Matcher[]) matchers);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.cryptomator.common.test.matcher;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeDiagnosingMatcher;
|
||||
|
||||
public class ExceptionMatcher<T extends Throwable> extends TypeSafeDiagnosingMatcher<T> {
|
||||
|
||||
public static <T extends Throwable> ExceptionMatcher<T> ofType(Class<T> exceptionType) {
|
||||
return new ExceptionMatcher<>(exceptionType);
|
||||
}
|
||||
|
||||
private final Class<T> exceptionType;
|
||||
private final Optional<Matcher<T>> subMatcher;
|
||||
|
||||
private ExceptionMatcher(Class<T> exceptionType) {
|
||||
super(exceptionType);
|
||||
this.exceptionType = exceptionType;
|
||||
this.subMatcher = Optional.empty();
|
||||
}
|
||||
|
||||
private ExceptionMatcher(Class<T> exceptionType, Matcher<T> subMatcher) {
|
||||
super(exceptionType);
|
||||
this.exceptionType = exceptionType;
|
||||
this.subMatcher = Optional.of(subMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
subMatcher.ifPresent(description::appendDescriptionOf);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(T item, Description mismatchDescription) {
|
||||
if (subMatcher.map(matcher -> !matcher.matches(item)).orElse(false)) {
|
||||
subMatcher.get().describeMismatch(item, mismatchDescription);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Matcher<T> withCauseThat(Matcher<? super Throwable> matcher) {
|
||||
return new ExceptionMatcher<T>(exceptionType, new PropertyMatcher<>(exceptionType, Throwable::getCause, "cause", matcher));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package org.cryptomator.common.test.matcher;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeDiagnosingMatcher;
|
||||
|
||||
public class OptionalMatcher {
|
||||
|
||||
public static <T> Matcher<Optional<T>> presentOptionalWithValueThat(Matcher<? super T> valueMatcher) {
|
||||
return new TypeSafeDiagnosingMatcher<Optional<T>>(Optional.class) {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description //
|
||||
.appendText("a present Optional with a value that ") //
|
||||
.appendDescriptionOf(valueMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Optional<T> item, Description mismatchDescription) {
|
||||
if (item.isPresent()) {
|
||||
if (valueMatcher.matches(item.get())) {
|
||||
return true;
|
||||
} else {
|
||||
mismatchDescription.appendText("a present Optional with value that ");
|
||||
valueMatcher.describeMismatch(item, mismatchDescription);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
mismatchDescription.appendText("an empty Optional");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Matcher<Optional<T>> emptyOptional() {
|
||||
return new TypeSafeDiagnosingMatcher<Optional<T>>(Optional.class) {
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("an empty Optional");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Optional<T> item, Description mismatchDescription) {
|
||||
if (item.isPresent()) {
|
||||
mismatchDescription.appendText("a present Optional of ").appendValue(item.get());
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.common.test.matcher;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeDiagnosingMatcher;
|
||||
|
||||
public class PropertyMatcher<T, P> extends TypeSafeDiagnosingMatcher<T> {
|
||||
|
||||
private final Class<T> expectedType;
|
||||
private final Function<? super T, P> getter;
|
||||
private final String name;
|
||||
private final Matcher<? super P> subMatcher;
|
||||
|
||||
public PropertyMatcher(Class<T> type, Function<? super T, P> getter, String name, Matcher<? super P> subMatcher) {
|
||||
super(type);
|
||||
this.expectedType = type;
|
||||
this.getter = getter;
|
||||
this.name = name;
|
||||
this.subMatcher = subMatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("a ") //
|
||||
.appendText(expectedType.getSimpleName()) //
|
||||
.appendText(" with a ") //
|
||||
.appendText(name) //
|
||||
.appendText(" that ") //
|
||||
.appendDescriptionOf(subMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(T item, Description mismatchDescription) {
|
||||
P propertyValue = getter.apply(item);
|
||||
if (subMatcher.matches(propertyValue)) {
|
||||
return true;
|
||||
} else {
|
||||
mismatchDescription.appendText("a ") //
|
||||
.appendText(expectedType.getSimpleName()) //
|
||||
.appendText(" with a ") //
|
||||
.appendText(name) //
|
||||
.appendText(" that ");
|
||||
subMatcher.describeMismatch(propertyValue, mismatchDescription);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package org.cryptomator.common.test.mockito;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
public class Answers {
|
||||
|
||||
public static <T> Answer<T> collectParameters(Answer<T> answer, Consumer<?>... parameterConsumers) {
|
||||
return new Answer<T>() {
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
@Override
|
||||
public T answer(InvocationOnMock invocation) throws Throwable {
|
||||
for (int i = 0; i < invocation.getArguments().length; i++) {
|
||||
if (parameterConsumers.length > i) {
|
||||
((Consumer) parameterConsumers[i]).accept(invocation.getArguments()[i]);
|
||||
}
|
||||
}
|
||||
return answer.answer(invocation);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> Answer<T> consecutiveAnswers(Answer<T>... answers) {
|
||||
if (answers == null || answers.length == 0) {
|
||||
throw new IllegalArgumentException("Required at least one answer");
|
||||
}
|
||||
if (asList(answers).contains(null)) {
|
||||
throw new IllegalArgumentException("No answers must be null");
|
||||
}
|
||||
return new Answer<T>() {
|
||||
private int nextIndex = 0;
|
||||
|
||||
@Override
|
||||
public T answer(InvocationOnMock invocation) throws Throwable {
|
||||
try {
|
||||
return answers[nextIndex].answer(invocation);
|
||||
} finally {
|
||||
nextIndex = (nextIndex + 1) % answers.length;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Answer<T> value(T value) {
|
||||
return new Answer<T>() {
|
||||
@Override
|
||||
public T answer(InvocationOnMock invocation) throws Throwable {
|
||||
return value;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.common;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
public final class LazyInitializer {
|
||||
|
||||
@@ -9,7 +10,7 @@ public final class LazyInitializer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
||||
* Same as {@link #initializeLazily(AtomicReference, SupplierThrowingException, Class)} except that no checked exception may be thrown by the factory function.
|
||||
*
|
||||
* @param <T> Type of the value
|
||||
* @param reference A reference to a maybe not yet initialized value.
|
||||
@@ -17,18 +18,59 @@ public final class LazyInitializer {
|
||||
* @return The initialized value
|
||||
*/
|
||||
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
|
||||
SupplierThrowingException<T, RuntimeException> factoryThrowingRuntimeExceptions = () -> factory.get();
|
||||
return initializeLazily(reference, factoryThrowingRuntimeExceptions, RuntimeException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509
|
||||
*
|
||||
* @param <T> Type of the value
|
||||
* @param <E> Type of the any expected exception that may occur during initialization
|
||||
* @param reference A reference to a maybe not yet initialized value.
|
||||
* @param factory A factory providing a value for the reference, if it doesn't exist yet. The factory may be invoked multiple times, but only one result will survive.
|
||||
* @param exceptionType Expected exception type.
|
||||
* @return The initialized value
|
||||
* @throws E Exception thrown by the factory function.
|
||||
*/
|
||||
public static <T, E extends Exception> T initializeLazily(AtomicReference<T> reference, SupplierThrowingException<T, E> factory, Class<E> exceptionType) throws E {
|
||||
final T existing = reference.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
} else {
|
||||
return reference.updateAndGet(currentValue -> {
|
||||
if (currentValue == null) {
|
||||
return factory.get();
|
||||
try {
|
||||
return reference.updateAndGet(invokeFactoryIfNull(factory));
|
||||
} catch (InitializationException e) {
|
||||
if (exceptionType.isInstance(e.getCause())) {
|
||||
throw exceptionType.cast(e.getCause());
|
||||
} else {
|
||||
return currentValue;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T, E extends Exception> UnaryOperator<T> invokeFactoryIfNull(SupplierThrowingException<T, E> factory) throws InitializationException {
|
||||
return currentValue -> {
|
||||
if (currentValue == null) {
|
||||
try {
|
||||
return factory.get();
|
||||
} catch (RuntimeException e) {
|
||||
throw e; // don't catch unchecked exceptions
|
||||
} catch (Exception e) {
|
||||
throw new InitializationException(e);
|
||||
}
|
||||
} else {
|
||||
return currentValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class InitializationException extends RuntimeException {
|
||||
|
||||
public InitializationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
2
main/filesystem-api/.gitignore
vendored
2
main/filesystem-api/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
/target/
|
||||
/target/
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2015 Markus Kreusch
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
<name>Cryptomator filesystem: API</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- apache commons -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons-test</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,55 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
class Copier {
|
||||
|
||||
public static void copy(Folder source, Folder destination) {
|
||||
assertFoldersAreNotNested(source, destination);
|
||||
|
||||
destination.delete();
|
||||
destination.create();
|
||||
|
||||
source.files().forEach(sourceFile -> {
|
||||
File destinationFile = destination.file(sourceFile.name());
|
||||
sourceFile.copyTo(destinationFile);
|
||||
});
|
||||
|
||||
source.folders().forEach(sourceFolder -> {
|
||||
Folder destinationFolder = destination.folder(sourceFolder.name());
|
||||
sourceFolder.copyTo(destinationFolder);
|
||||
});
|
||||
}
|
||||
|
||||
public static void copy(File source, File destination) {
|
||||
try (OpenFiles openFiles = DeadlockSafeFileOpener.withReadable(source).andWritable(destination).open()) {
|
||||
ReadableFile readable = openFiles.readable(source);
|
||||
WritableFile writable = openFiles.writable(destination);
|
||||
writable.truncate();
|
||||
ByteStreams.copy(readable, writable);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertFoldersAreNotNested(Folder source, Folder destination) {
|
||||
if (source.isAncestorOf(destination)) {
|
||||
throw new IllegalArgumentException("Can not copy parent to child directory (src: " + source + ", dst: " + destination + ")");
|
||||
}
|
||||
if (destination.isAncestorOf(source)) {
|
||||
throw new IllegalArgumentException("Can not copy child to parent directory (src: " + source + ", dst: " + destination + ")");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DeadlockSafeFileOpener {
|
||||
|
||||
public static DeadlockSafeFileOpener withReadable(File file) {
|
||||
return new DeadlockSafeFileOpener().andReadable(file);
|
||||
}
|
||||
|
||||
public static DeadlockSafeFileOpener withWritable(File file) {
|
||||
return new DeadlockSafeFileOpener().andWritable(file);
|
||||
}
|
||||
|
||||
private final SortedMap<File, Consumer<File>> filesWithOperation = new TreeMap<>();
|
||||
|
||||
private final Map<File, ReadableFile> readableFiles = new HashMap<>();
|
||||
private final Map<File, WritableFile> writableFiles = new HashMap<>();
|
||||
|
||||
private DeadlockSafeFileOpener() {
|
||||
}
|
||||
|
||||
public DeadlockSafeFileOpener andReadable(File file) {
|
||||
if (filesWithOperation.put(file, this::openReadable) != null) {
|
||||
throw new IllegalArgumentException(format("File %s already marked for opening", file));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public DeadlockSafeFileOpener andWritable(File file) {
|
||||
if (filesWithOperation.put(file, this::openWritable) != null) {
|
||||
throw new IllegalArgumentException(format("File %s already marked for opening", file));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void openReadable(File file) {
|
||||
readableFiles.put(file, file.openReadable());
|
||||
}
|
||||
|
||||
private void openWritable(File file) {
|
||||
writableFiles.put(file, file.openWritable());
|
||||
}
|
||||
|
||||
public OpenFiles open() {
|
||||
try {
|
||||
filesWithOperation.forEach((file, openAction) -> openAction.accept(file));
|
||||
} catch (RuntimeException e) {
|
||||
OpenFiles.cleanup(readableFiles.values(), writableFiles.values());
|
||||
throw e;
|
||||
}
|
||||
return new OpenFiles(readableFiles, writableFiles);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
public class Deleter {
|
||||
|
||||
/**
|
||||
* Deletes all and only the content of a given {@link Folder} but <b>not</b> the folder itself.
|
||||
*/
|
||||
public static void deleteContent(Folder folder) {
|
||||
if (folder.exists()) {
|
||||
folder.folders().forEach(Folder::delete);
|
||||
folder.files().forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
/**
|
||||
* A {@link File} in a {@link FileSystem}.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public interface File extends Node, Comparable<File> {
|
||||
|
||||
static final int EOF = -1;
|
||||
|
||||
/**
|
||||
* @return The current size of the file. This value is a snapshot and might have been changed by concurrent modifications.
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
long size() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Opens this file for reading.
|
||||
* <p>
|
||||
* An implementation guarantees, that per {@link FileSystem} and
|
||||
* {@code File} while a {@code ReadableFile} is open no {@link WritableFile}
|
||||
* can be open and vice versa. A {@link ReadableFile} is open when returned
|
||||
* from this method and not yet closed using {@link ReadableFile#close()}.
|
||||
* <br>
|
||||
* A limitation to the number of {@code ReadableFiles} is in general not
|
||||
* required but may be set by a specific implementation.
|
||||
* <p>
|
||||
* If a {@link WritableFile} for this {@code File} is open the invocation of
|
||||
* this method will block regarding the specified timeout.<br>
|
||||
* In addition implementations may block to lock the required IO resources
|
||||
* to read the file.
|
||||
*
|
||||
* @return a {@link ReadableFile} to work with
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while opening the file, the
|
||||
* file does not exist or is a directory
|
||||
*/
|
||||
ReadableFile openReadable() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Opens this file for writing.
|
||||
* <p>
|
||||
* If the file does not exist a new empty file is created.
|
||||
* <p>
|
||||
* An implementation guarantees, that per {@link FileSystem} and
|
||||
* {@code File} only one {@link WritableFile} is open at a time. A
|
||||
* {@code WritableFile} is open when returned from this method and not yet
|
||||
* closed using {@link WritableFile#close()} or
|
||||
* {@link WritableFile#delete()}.<br>
|
||||
* In addition while a {@code WritableFile} is open no {@link ReadableFile}
|
||||
* can be open and vice versa.
|
||||
* <p>
|
||||
* If a {@code Readable-} or {@code WritableFile} for this {@code File} is
|
||||
* open the invocation of this method will block regarding the specified
|
||||
* timeout.<br>
|
||||
* In addition implementations may block to lock the required IO resources
|
||||
* to read the file.
|
||||
*
|
||||
* @return a {@link WritableFile} to work with
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while opening the file or
|
||||
* the file is a directory
|
||||
*/
|
||||
WritableFile openWritable() throws UncheckedIOException;
|
||||
|
||||
default void copyTo(File destination) {
|
||||
Copier.copy(this, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves this file including content to a new location specified by <code>destination</code>.
|
||||
*/
|
||||
void moveTo(File destination) throws UncheckedIOException;
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* The root folder of a file system.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public interface FileSystem extends Folder {
|
||||
|
||||
/**
|
||||
* @return an empty {@link Optional} because a {@link FileSystem} represents
|
||||
* the root {@link Folder} and thus does not have a parent
|
||||
*/
|
||||
@Override
|
||||
default Optional<? extends Folder> parent() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Long> quotaUsedBytes();
|
||||
|
||||
Optional<Long> quotaAvailableBytes();
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link Folder} in a {@link FileSystem}.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
*/
|
||||
public interface Folder extends Node {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Creates a {@link Stream} over all child nodes of this {@code Folder}.
|
||||
* <p>
|
||||
* <b>Note:</b> The {@link Stream} may be lazily populated and thus
|
||||
* {@link IOException IOExceptions} may occurs after this method returned.
|
||||
* In this case implementors should throw a {@link UncheckedIOException}
|
||||
* from any method that produces an {@link IOException}. Thus users should
|
||||
* expect {@link UncheckedIOException UncheckedIOExceptions} when invoking
|
||||
* methods on the returned {@code Stream}.
|
||||
*
|
||||
* @return the created {@code Stream}
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while initializing the
|
||||
* stream or the {@code Folder} does not exist
|
||||
*/
|
||||
Stream<? extends Node> children() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Returns the child {@link Node} in this directory of type {@link File}
|
||||
* with the specified name.
|
||||
* <p>
|
||||
* This operation always returns a {@link File} without checking if the file
|
||||
* exists or is a {@link Folder} instead.
|
||||
*/
|
||||
File file(String name) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Returns a file by resolving a path relative to this folder.
|
||||
*
|
||||
* @param path A unix-style path, which is always relative to this folder, no matter if it starts with a slash or not. Path must not be empty.
|
||||
* @return File with the given path relative to this folder
|
||||
* @throws IllegalArgumentException
|
||||
* if relativePath is empty
|
||||
*/
|
||||
default File resolveFile(String relativePath) throws UncheckedIOException, IllegalArgumentException {
|
||||
return PathResolver.resolveFile(this, relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Returns the child {@link Node} in this directory of type {@link Folder}
|
||||
* with the specified name.
|
||||
* <p>
|
||||
* This operation always returns a {@link Folder} without checking if the
|
||||
* folder exists or is a {@link File} instead.
|
||||
*/
|
||||
Folder folder(String name) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Returns a folder by resolving a path relative to this folder.
|
||||
*
|
||||
* @param path A unix-style path, which is always relative to this folder, no matter if it starts with a slash or not. Path may be empty.
|
||||
* @return Folder with the given path relative to this folder. Returns <code>this</code> if path is empty.
|
||||
*/
|
||||
default Folder resolveFolder(String relativePath) throws UncheckedIOException {
|
||||
return PathResolver.resolveFolder(this, relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the directory including all parent directories, if it doesn't
|
||||
* exist yet. No effect, if folder already exists.
|
||||
*
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while creating the folder or
|
||||
* one of its parents
|
||||
*/
|
||||
void create() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Recusively copies this directory and all its contents to (not into) the
|
||||
* given destination, creating nonexisting parent directories. If the target
|
||||
* exists it is deleted before performing the copy.
|
||||
*
|
||||
* @param target
|
||||
* Destination folder. Must not be a descendant of this folder.
|
||||
*/
|
||||
default void copyTo(Folder target) throws UncheckedIOException {
|
||||
Copier.copy(this, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves this directory and its contents to the given destination. If the
|
||||
* target exists it is deleted before performing the move.
|
||||
*/
|
||||
void moveTo(Folder target);
|
||||
|
||||
/**
|
||||
* @return the result of {@link #children()} filtered to contain only
|
||||
* {@link File Files}
|
||||
*/
|
||||
default Stream<? extends File> files() throws UncheckedIOException {
|
||||
return children() //
|
||||
.filter(File.class::isInstance) //
|
||||
.map(File.class::cast);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the result of {@link #children()} filtered to contain only
|
||||
* {@link Folder Folders}
|
||||
*/
|
||||
default Stream<? extends Folder> folders() throws UncheckedIOException {
|
||||
return children() //
|
||||
.filter(Folder.class::isInstance) //
|
||||
.map(Folder.class::cast);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively checks whether this folder or any subfolder contains the
|
||||
* given node.
|
||||
*
|
||||
* @param node
|
||||
* Potential child, grandchild, ...
|
||||
* @return <code>true</code> if this folder is an ancestor of the node.
|
||||
*/
|
||||
default boolean isAncestorOf(Node node) {
|
||||
if (!node.parent().isPresent()) {
|
||||
return false;
|
||||
} else if (node.parent().get().equals(this)) {
|
||||
return true;
|
||||
} else {
|
||||
return this.isAncestorOf(node.parent().get());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class FolderVisitor {
|
||||
|
||||
private final Consumer<Folder> beforeFolderVisitor;
|
||||
private final Consumer<Folder> afterFolderVisitor;
|
||||
private final Consumer<File> fileVisitor;
|
||||
private final Consumer<Node> nodeVisitor;
|
||||
private final int maxDepth;
|
||||
|
||||
public FolderVisitor(FolderVisitorBuilder builder) {
|
||||
this.beforeFolderVisitor = builder.beforeFolderVisitor;
|
||||
this.afterFolderVisitor = builder.afterFolderVisitor;
|
||||
this.fileVisitor = builder.fileVisitor;
|
||||
this.nodeVisitor = builder.nodeVisitor;
|
||||
this.maxDepth = builder.maxDepth;
|
||||
}
|
||||
|
||||
public static FolderVisitorBuilder folderVisitor() {
|
||||
return new FolderVisitorBuilder();
|
||||
}
|
||||
|
||||
public FolderVisitor visit(Folder folder) {
|
||||
return visit(folder, 0);
|
||||
}
|
||||
|
||||
public FolderVisitor visit(File file) {
|
||||
return visit(file, 0);
|
||||
}
|
||||
|
||||
private FolderVisitor visit(Folder folder, int depth) {
|
||||
beforeFolderVisitor.accept(folder);
|
||||
nodeVisitor.accept(folder);
|
||||
final int childDepth = depth + 1;
|
||||
if (childDepth <= maxDepth) {
|
||||
folder.folders().forEach(childFolder -> visit(childFolder, childDepth));
|
||||
folder.files().forEach(childFile -> visit(childFile, childDepth));
|
||||
}
|
||||
afterFolderVisitor.accept(folder);
|
||||
return this;
|
||||
}
|
||||
|
||||
private FolderVisitor visit(File file, int depth) {
|
||||
nodeVisitor.accept(file);
|
||||
fileVisitor.accept(file);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static class FolderVisitorBuilder {
|
||||
|
||||
private Consumer<Folder> beforeFolderVisitor = noOp();
|
||||
private Consumer<Folder> afterFolderVisitor = noOp();
|
||||
private Consumer<File> fileVisitor = noOp();
|
||||
private Consumer<Node> nodeVisitor = noOp();
|
||||
private int maxDepth = Integer.MAX_VALUE;
|
||||
|
||||
private FolderVisitorBuilder() {
|
||||
}
|
||||
|
||||
public FolderVisitorBuilder beforeFolder(Consumer<Folder> beforeFolderVisitor) {
|
||||
if (beforeFolderVisitor == null) {
|
||||
throw new IllegalArgumentException("Vistior may not be null");
|
||||
}
|
||||
this.beforeFolderVisitor = beforeFolderVisitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FolderVisitorBuilder afterFolder(Consumer<Folder> afterFolderVisitor) {
|
||||
if (afterFolderVisitor == null) {
|
||||
throw new IllegalArgumentException("Vistior may not be null");
|
||||
}
|
||||
this.afterFolderVisitor = afterFolderVisitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FolderVisitorBuilder forEachFile(Consumer<File> fileVisitor) {
|
||||
if (fileVisitor == null) {
|
||||
throw new IllegalArgumentException("Vistior may not be null");
|
||||
}
|
||||
this.fileVisitor = fileVisitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FolderVisitorBuilder forEachNode(Consumer<Node> nodeVisitor) {
|
||||
if (nodeVisitor == null) {
|
||||
throw new IllegalArgumentException("Vistior may not be null");
|
||||
}
|
||||
this.nodeVisitor = nodeVisitor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FolderVisitorBuilder withMaxDepth(int maxDepth) {
|
||||
if (maxDepth < 0) {
|
||||
throw new IllegalArgumentException(format("maxDepth must not be smaller 0 but was %d", maxDepth));
|
||||
}
|
||||
this.maxDepth = maxDepth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FolderVisitor visit(Folder folder) {
|
||||
return build().visit(folder);
|
||||
}
|
||||
|
||||
public FolderVisitor visit(File file) {
|
||||
return build().visit(file);
|
||||
}
|
||||
|
||||
public FolderVisitor build() {
|
||||
return new FolderVisitor(this);
|
||||
}
|
||||
|
||||
private static <T> Consumer<T> noOp() {
|
||||
return ignoredParameter -> {
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a node, namely a {@link File} or {@link Folder}, in a
|
||||
* {@link FileSystem}.
|
||||
* <p>
|
||||
* A node's identity (i.e. {@link #hashCode()} and {@link #equals(Object)}) depends on its parent node and its name (forming the node's path).
|
||||
* These properties are meant to be immutable. This means that e.g. moving a node doesn't modify the node's identity but rather transfers properties to the destination node.
|
||||
*
|
||||
* @author Markus Kreusch
|
||||
* @see Folder
|
||||
* @see File
|
||||
*/
|
||||
public interface Node {
|
||||
|
||||
String name() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* @return Optional parent folder. No parent is present for the root node (see {@link FileSystem#parent()}).
|
||||
*/
|
||||
Optional<? extends Folder> parent() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the node exists.
|
||||
*/
|
||||
boolean exists() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Deletes the node if it exists.
|
||||
* <p>
|
||||
* Does nothing if the node does not exist.
|
||||
*/
|
||||
void delete() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines the last modified date of this node.
|
||||
*
|
||||
* @returns the last modified date of the file
|
||||
*/
|
||||
Instant lastModified() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Sets the last modified date of the file.
|
||||
*
|
||||
* @param lastModified the time to set as creation time
|
||||
*/
|
||||
void setLastModified(Instant lastModified) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines the creation time of this node.
|
||||
* <p>
|
||||
* Note: Getting the creation time may not be supported by all {@link FileSystem FileSystems}.
|
||||
*
|
||||
* @returns the creation time of the file or {@link Optional#empty()} if not supported
|
||||
*/
|
||||
default Optional<Instant> creationTime() throws UncheckedIOException {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Sets the creation time of this node.
|
||||
* <p>
|
||||
* Setting the creation time may not be supported by all {@link FileSystem FileSystems}. If the {@code FileSystem} this {@code Node} belongs to does not support the
|
||||
* setting the creation time the behavior of this method is unspecified.
|
||||
*
|
||||
* @param creationTime the time to set as creation time
|
||||
*/
|
||||
default void setCreationTime(Instant creationTime) throws UncheckedIOException {
|
||||
throw new UncheckedIOException(new IOException("CreationTime not supported"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link FileSystem} this Node belongs to
|
||||
*/
|
||||
default FileSystem fileSystem() {
|
||||
return parent() //
|
||||
.map(Node::fileSystem) //
|
||||
.orElseGet(() -> (FileSystem) this);
|
||||
}
|
||||
|
||||
default boolean belongsToSameFilesystem(Node other) {
|
||||
return fileSystem() == other.fileSystem();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenFiles implements AutoCloseable {
|
||||
|
||||
private final static Logger LOG = LoggerFactory.getLogger(OpenFiles.class);
|
||||
|
||||
private final Map<File, ReadableFile> readableFiles;
|
||||
private final Map<File, WritableFile> writableFiles;
|
||||
|
||||
public OpenFiles(Map<File, ReadableFile> readableFiles, Map<File, WritableFile> writableFiles) {
|
||||
this.readableFiles = readableFiles;
|
||||
this.writableFiles = writableFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
OpenFiles.cleanup(readableFiles.values(), writableFiles.values());
|
||||
}
|
||||
|
||||
public ReadableFile readable(File file) {
|
||||
return readableFiles.computeIfAbsent(file, fileNotOpenForReading -> {
|
||||
throw new IllegalArgumentException(String.format("File %s is not open for reading", fileNotOpenForReading));
|
||||
});
|
||||
}
|
||||
|
||||
public WritableFile writable(File file) {
|
||||
return writableFiles.computeIfAbsent(file, fileNotOpenForWriting -> {
|
||||
throw new IllegalArgumentException(String.format("File %s is not open for writing", fileNotOpenForWriting));
|
||||
});
|
||||
}
|
||||
|
||||
static void cleanup(Collection<ReadableFile> readableFiles, Collection<WritableFile> writableFiles) {
|
||||
Iterator<? extends AutoCloseable> iterator = Stream.concat(readableFiles.stream(), writableFiles.stream()).iterator();
|
||||
UncheckedIOException firstException = null;
|
||||
while (iterator.hasNext()) {
|
||||
AutoCloseable openFile = iterator.next();
|
||||
try {
|
||||
openFile.close();
|
||||
} catch (UncheckedIOException e) {
|
||||
if (firstException == null) {
|
||||
firstException = e;
|
||||
} else {
|
||||
firstException.addSuppressed(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected exception during close on " + openFile.getClass().getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
if (firstException != null) {
|
||||
throw firstException;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
final class PathResolver {
|
||||
|
||||
private static final String DOT = ".";
|
||||
private static final String DOTDOT = "..";
|
||||
|
||||
private PathResolver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a relative path (separated by '/') to a folder, e.g.
|
||||
* <!-- @formatter:off -->
|
||||
* <table>
|
||||
* <thead>
|
||||
* <tr>
|
||||
* <th>dir</th>
|
||||
* <th>path</th>
|
||||
* <th>result</th>
|
||||
* </tr>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td>foo/bar</td>
|
||||
* <td>/foo/bar/foo/bar</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td>../baz</td>
|
||||
* <td>/foo/baz</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td>./foo/..</td>
|
||||
* <td>/foo/bar</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td>/</td>
|
||||
* <td>/foo/bar</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td></td>
|
||||
* <td>/foo/bar</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>/foo/bar</td>
|
||||
* <td>../../..</td>
|
||||
* <td>Exception</td>
|
||||
* </tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
*
|
||||
* @param dir The directory from which to resolve the path.
|
||||
* @param relativePath The path relative to a given directory.
|
||||
* @return The folder with the given path relative to the given dir.
|
||||
*/
|
||||
public static Folder resolveFolder(Folder dir, String relativePath) {
|
||||
final String[] fragments = StringUtils.split(relativePath, '/');
|
||||
if (ArrayUtils.isEmpty(fragments)) {
|
||||
return dir;
|
||||
}
|
||||
return resolveFolder(dir, Arrays.stream(fragments).iterator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a relative path (separated by '/') to a file. Besides returning a File, this method is identical to {@link #resolveFile(Folder, String)}.
|
||||
*
|
||||
* @param dir The directory from which to resolve the path.
|
||||
* @param relativePath The path relative to a given directory.
|
||||
* @return The file with the given path relative to the given dir.
|
||||
* @throws IllegalArgumentException
|
||||
* if relativePath is empty, as this path would resolve to the directory itself, which obviously can't be a file.
|
||||
*/
|
||||
public static File resolveFile(Folder dir, String relativePath) {
|
||||
final String[] fragments = StringUtils.split(relativePath, '/');
|
||||
if (ArrayUtils.isEmpty(fragments)) {
|
||||
throw new IllegalArgumentException("Empty relativePath");
|
||||
}
|
||||
final Folder folder = resolveFolder(dir, Arrays.stream(fragments).limit(fragments.length - 1).iterator());
|
||||
final String filename = fragments[fragments.length - 1];
|
||||
return folder.file(filename);
|
||||
}
|
||||
|
||||
private static Folder resolveFolder(Folder dir, Iterator<String> remainingPathFragments) {
|
||||
if (!remainingPathFragments.hasNext()) {
|
||||
return dir;
|
||||
}
|
||||
final String fragment = remainingPathFragments.next();
|
||||
assert fragment.length() > 0 : "iterator must not contain empty fragments";
|
||||
if (DOT.equals(fragment)) {
|
||||
return resolveFolder(dir, remainingPathFragments);
|
||||
} else if (DOTDOT.equals(fragment) && dir.parent().isPresent()) {
|
||||
return resolveFolder(dir.parent().get(), remainingPathFragments);
|
||||
} else if (DOTDOT.equals(fragment) && !dir.parent().isPresent()) {
|
||||
throw new UncheckedIOException(new FileNotFoundException("Unresolvable path"));
|
||||
} else {
|
||||
return resolveFolder(dir.folder(fragment), remainingPathFragments);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
public interface ReadableFile extends ReadableByteChannel {
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Tries to fill the remaining space in the given byte buffer with data from
|
||||
* this readable bytes from the current position.
|
||||
* <p>
|
||||
* May read less bytes if the end of this readable bytes has been reached.
|
||||
*
|
||||
* @param target
|
||||
* the byte buffer to fill
|
||||
* @return the number of bytes actually read, or {@code -1} if the end of
|
||||
* file has been reached
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while reading from this
|
||||
* {@code ReadableBytes}
|
||||
*/
|
||||
@Override
|
||||
int read(ByteBuffer target) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Fast-forwards or rewinds the file to the specified position.
|
||||
* <p>
|
||||
* Consecutive reads on the file will begin at the new position.
|
||||
*
|
||||
* @param position
|
||||
* the position to set the file to
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*
|
||||
*/
|
||||
void position(long position) throws UncheckedIOException;
|
||||
|
||||
@Override
|
||||
void close() throws UncheckedIOException;
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Markus Kreusch
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
******************************************************************************/
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
|
||||
public interface WritableFile extends WritableByteChannel {
|
||||
|
||||
void truncate() throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Writes the data in the given byte buffer to this readable bytes at the
|
||||
* current position.
|
||||
*
|
||||
* @param source
|
||||
* the byte buffer to use
|
||||
* @return the number of bytes written, always equal to
|
||||
* {@code source.remaining()}
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs while writing
|
||||
*/
|
||||
@Override
|
||||
int write(ByteBuffer source) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Fast-forwards or rewinds the file to the specified position.
|
||||
* <p>
|
||||
* Consecutive writes on the file will begin at the new position.
|
||||
* <p>
|
||||
* If the position is set to a value greater than the current end of file
|
||||
* consecutive writes will write data to the given position. The value of
|
||||
* all bytes between this position and the previous end of file will be
|
||||
* unspecified.
|
||||
*
|
||||
* @param position
|
||||
* the position to set the file to
|
||||
* @throws UncheckedIOException
|
||||
* if an {@link IOException} occurs
|
||||
*/
|
||||
void position(long position) throws UncheckedIOException;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Closes this {@code WritableFile} which finally commits all operations
|
||||
* performed on it to the underlying file system.
|
||||
* <p>
|
||||
* After a {@code WritableFile} has been closed all other operations will
|
||||
* throw an {@link UncheckedIOException}.
|
||||
* <p>
|
||||
* Invoking this method on a {@link WritableFile} which has already been
|
||||
* closed does nothing.
|
||||
*/
|
||||
@Override
|
||||
void close() throws UncheckedIOException;
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
public abstract class DelegatingFile<D extends DelegatingFolder<D, ?>> extends DelegatingNode<File> implements File {
|
||||
|
||||
private final D parent;
|
||||
|
||||
public DelegatingFile(D parent, File delegate) {
|
||||
super(delegate);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<D> parent() throws UncheckedIOException {
|
||||
return Optional.of(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() throws UncheckedIOException {
|
||||
return delegate.openReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableFile openWritable() throws UncheckedIOException {
|
||||
return delegate.openWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(File destination) {
|
||||
if (getClass().equals(destination.getClass())) {
|
||||
final File delegateDest = ((DelegatingFile<?>) destination).delegate;
|
||||
delegate.copyTo(delegateDest);
|
||||
} else {
|
||||
delegate.copyTo(destination);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(File destination) {
|
||||
if (getClass().equals(destination.getClass())) {
|
||||
final File delegateDest = ((DelegatingFile<?>) destination).delegate;
|
||||
delegate.moveTo(delegateDest);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can only move DelegatingFile to other DelegatingFile.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() throws UncheckedIOException {
|
||||
delegate.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(File o) {
|
||||
if (getClass().equals(o.getClass())) {
|
||||
final File delegateOther = ((DelegatingFile<?>) o).delegate;
|
||||
return delegate.compareTo(delegateOther);
|
||||
} else {
|
||||
return delegate.compareTo(o);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
public interface DelegatingFileSystem extends FileSystem {
|
||||
|
||||
Folder getDelegate();
|
||||
|
||||
@Override
|
||||
default Optional<Long> quotaUsedBytes() {
|
||||
return getDelegate().fileSystem().quotaUsedBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
default Optional<Long> quotaAvailableBytes() {
|
||||
return getDelegate().fileSystem().quotaAvailableBytes();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.cryptomator.common.WeakValuedCache;
|
||||
import org.cryptomator.common.streams.AutoClosingStream;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
public abstract class DelegatingFolder<D extends DelegatingFolder<D, F>, F extends DelegatingFile<D>> extends DelegatingNode<Folder>implements Folder {
|
||||
|
||||
private final D parent;
|
||||
private final WeakValuedCache<Folder, D> folders = WeakValuedCache.usingLoader(this::newFolder);
|
||||
private final WeakValuedCache<File, F> files = WeakValuedCache.usingLoader(this::newFile);
|
||||
|
||||
public DelegatingFolder(D parent, Folder delegate) {
|
||||
super(delegate);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<D> parent() throws UncheckedIOException {
|
||||
return Optional.ofNullable(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<? extends Node> children() throws UncheckedIOException {
|
||||
return AutoClosingStream.from(Stream.concat(folders(), files()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<D> folders() {
|
||||
return delegate.folders().map(folders::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<F> files() throws UncheckedIOException {
|
||||
return delegate.files().map(files::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public F file(String name) throws UncheckedIOException {
|
||||
return files.get(delegate.file(name));
|
||||
}
|
||||
|
||||
protected abstract F newFile(File delegate);
|
||||
|
||||
@Override
|
||||
public D folder(String name) throws UncheckedIOException {
|
||||
return folders.get(delegate.folder(name));
|
||||
}
|
||||
|
||||
protected abstract D newFolder(Folder delegate);
|
||||
|
||||
@Override
|
||||
public void create() throws UncheckedIOException {
|
||||
if (exists()) {
|
||||
return;
|
||||
}
|
||||
parent().ifPresent(p -> p.create());
|
||||
delegate.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
delegate.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(Folder destination) throws UncheckedIOException {
|
||||
if (destination instanceof DelegatingFolder) {
|
||||
final Folder delegateDest = ((DelegatingFolder<?, ?>) destination).delegate;
|
||||
delegate.copyTo(delegateDest);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can only copy DelegatingFolder to other DelegatingFolder.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Folder destination) {
|
||||
if (getClass().equals(destination.getClass())) {
|
||||
final Folder delegateDest = ((DelegatingFolder<?, ?>) destination).delegate;
|
||||
delegate.moveTo(delegateDest);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can only move DelegatingFolder to other DelegatingFolder.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.Node;
|
||||
|
||||
public abstract class DelegatingNode<T extends Node> implements Node {
|
||||
|
||||
protected final T delegate;
|
||||
|
||||
public DelegatingNode(T delegate) {
|
||||
if (delegate == null) {
|
||||
throw new IllegalArgumentException("Delegate must not be null");
|
||||
}
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return delegate.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() throws UncheckedIOException {
|
||||
return delegate.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() throws UncheckedIOException {
|
||||
return delegate.lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastModified(Instant instant) throws UncheckedIOException {
|
||||
delegate.setLastModified(instant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> creationTime() throws UncheckedIOException {
|
||||
return delegate.creationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreationTime(Instant instant) throws UncheckedIOException {
|
||||
delegate.setCreationTime(instant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof DelegatingNode) {
|
||||
DelegatingNode<?> other = (DelegatingNode<?>) obj;
|
||||
return this.delegate.equals(other.delegate);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Delegate[" + delegate + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
|
||||
public class DelegatingReadableFile implements ReadableFile {
|
||||
|
||||
private final ReadableFile delegate;
|
||||
|
||||
public DelegatingReadableFile(ReadableFile delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return delegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer target) throws UncheckedIOException {
|
||||
return delegate.read(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
delegate.position(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
public class DelegatingWritableFile implements WritableFile {
|
||||
|
||||
final WritableFile delegate;
|
||||
|
||||
public DelegatingWritableFile(WritableFile delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return delegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void truncate() throws UncheckedIOException {
|
||||
delegate.truncate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer source) throws UncheckedIOException {
|
||||
return delegate.write(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long position) throws UncheckedIOException {
|
||||
delegate.position(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* Defines a file system abstraction to allow access to real and virtual file
|
||||
* systems through a common API.
|
||||
*/
|
||||
package org.cryptomator.filesystem;
|
||||
@@ -1,35 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class ByteBuffers {
|
||||
|
||||
private ByteBuffers() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies as many bytes as possible from the given source to the destination buffer.
|
||||
* The position of both buffers will be incremented by as many bytes as have been copied.
|
||||
*
|
||||
* @param source ByteBuffer from which bytes are read
|
||||
* @param destination ByteBuffer into which bytes are written
|
||||
* @return number of bytes copied, i.e. {@link ByteBuffer#remaining() source.remaining()} or {@link ByteBuffer#remaining() destination.remaining()}, whatever is less.
|
||||
*/
|
||||
public static int copy(ByteBuffer source, ByteBuffer destination) {
|
||||
final int numBytes = Math.min(source.remaining(), destination.remaining());
|
||||
final ByteBuffer tmp = source.asReadOnlyBuffer();
|
||||
tmp.limit(tmp.position() + numBytes);
|
||||
destination.put(tmp);
|
||||
source.position(tmp.position()); // until now only tmp pos has been incremented, so we need to adjust the position
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.cryptomator.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
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 ( //
|
||||
ReadableByteChannel channel = file.openReadable(); //
|
||||
Reader reader = Channels.newReader(channel, 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeDiagnosingMatcher;
|
||||
|
||||
public class ByteBufferMatcher {
|
||||
|
||||
public static Matcher<ByteBuffer> byteBufferFilledWith(int value) {
|
||||
if (((byte) value) != value) {
|
||||
throw new IllegalArgumentException("Invalid byte value");
|
||||
}
|
||||
return new TypeSafeDiagnosingMatcher<ByteBuffer>(ByteBuffer.class) {
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("a byte buffer filled with " + value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(ByteBuffer item, Description mismatchDescription) {
|
||||
while (item.hasRemaining()) {
|
||||
byte currentValue = item.get();
|
||||
if (currentValue != value) {
|
||||
mismatchDescription.appendText("a byte buffer containing also " + currentValue);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.cryptomator.common.test.matcher.ContainsMatcher.contains;
|
||||
import static org.cryptomator.common.test.mockito.Answers.collectParameters;
|
||||
import static org.cryptomator.common.test.mockito.Answers.consecutiveAnswers;
|
||||
import static org.cryptomator.common.test.mockito.Answers.value;
|
||||
import static org.cryptomator.filesystem.File.EOF;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import de.bechte.junit.runners.context.HierarchicalContextRunner;
|
||||
|
||||
@RunWith(HierarchicalContextRunner.class)
|
||||
public class CopierTest {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Rule
|
||||
public MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
|
||||
public class CopyFiles {
|
||||
|
||||
@Mock
|
||||
private File source;
|
||||
|
||||
@Mock
|
||||
private File destination;
|
||||
|
||||
@Mock
|
||||
private ReadableFile readable;
|
||||
|
||||
@Mock
|
||||
private WritableFile writable;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(source.openReadable()).thenReturn(readable);
|
||||
when(destination.openWritable()).thenReturn(writable);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFileOpensFilesInSortedOrderIfSourceIsSmallerDestination() {
|
||||
mockCompareToWithOrder(source, destination);
|
||||
when(readable.read(any())).thenReturn(EOF);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
InOrder inOrder = inOrder(source, destination);
|
||||
inOrder.verify(source).openReadable();
|
||||
inOrder.verify(destination).openWritable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFileOpensFilesInSortedOrderIfDestinationIsSmallerSource() {
|
||||
mockCompareToWithOrder(destination, source);
|
||||
when(readable.read(any())).thenReturn(EOF);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
InOrder inOrder = inOrder(source, destination);
|
||||
inOrder.verify(destination).openWritable();
|
||||
inOrder.verify(source).openReadable();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFileReadsAndWritesReadableSourceAndWritableDestintationUntilEof() {
|
||||
int irrelevantValue = 0;
|
||||
Collection<byte[]> written = new ArrayList<>();
|
||||
mockCompareToWithOrder(source, destination);
|
||||
byte[] read1 = {1, 48, 32, 33, 22};
|
||||
byte[] read2 = {4, 3, 1, -2, -8};
|
||||
when(readable.read(any())).then(consecutiveAnswers(fillBufferWith(read1), fillBufferWith(read2), value(EOF)));
|
||||
when(writable.write(any())).then(collectParameters(value(irrelevantValue), (ByteBuffer buffer) -> {
|
||||
byte[] data = new byte[buffer.remaining()];
|
||||
buffer.get(data);
|
||||
written.add(data);
|
||||
}));
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
InOrder inOrder = inOrder(readable, writable);
|
||||
inOrder.verify(writable).truncate();
|
||||
inOrder.verify(readable).read(any());
|
||||
inOrder.verify(writable).write(any());
|
||||
inOrder.verify(readable).read(any());
|
||||
inOrder.verify(writable).write(any());
|
||||
inOrder.verify(readable).read(any());
|
||||
inOrder.verify(readable).close();
|
||||
inOrder.verify(writable).close();
|
||||
|
||||
assertThat(written, contains(is(read1), is(read2)));
|
||||
}
|
||||
|
||||
private Answer<Integer> fillBufferWith(byte[] data) {
|
||||
return new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocation) throws Throwable {
|
||||
ByteBuffer buffer = invocation.getArgumentAt(0, ByteBuffer.class);
|
||||
for (byte value : data) {
|
||||
buffer.put(value);
|
||||
}
|
||||
return data.length;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private void mockCompareToWithOrder(File first, File last) {
|
||||
when(first.compareTo(last)).thenReturn(-1);
|
||||
when(last.compareTo(first)).thenReturn(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class CopyFolders {
|
||||
|
||||
@Mock
|
||||
private Folder source;
|
||||
|
||||
@Mock
|
||||
private Folder destination;
|
||||
|
||||
@Test
|
||||
public void testCopyFolderDeletesAndCreatesDestinationBeforeIteratingOverTheFilesAndFoldersInSource() {
|
||||
when(source.files()).thenReturn(Stream.empty());
|
||||
when(source.folders()).thenReturn(Stream.empty());
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
InOrder inOrder = inOrder(source, destination);
|
||||
inOrder.verify(destination).delete();
|
||||
inOrder.verify(destination).create();
|
||||
inOrder.verify(source).files();
|
||||
inOrder.verify(source).folders();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void testCopyFolderInvokesCopyToOnAllFilesInSourceWithFileWithSameNameFromDestination() {
|
||||
String filename1 = "nameOfFile1";
|
||||
String filename2 = "nameOfFile2";
|
||||
File file1 = mock(File.class);
|
||||
File file2 = mock(File.class);
|
||||
File destinationFile1 = mock(File.class);
|
||||
File destinationFile2 = mock(File.class);
|
||||
when(source.files()).thenReturn((Stream) asList(file1, file2).stream());
|
||||
when(source.folders()).thenReturn(Stream.empty());
|
||||
when(destination.file(filename1)).thenReturn(destinationFile1);
|
||||
when(destination.file(filename2)).thenReturn(destinationFile2);
|
||||
when(file1.name()).thenReturn(filename1);
|
||||
when(file2.name()).thenReturn(filename2);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
verify(file1).copyTo(destinationFile1);
|
||||
verify(file2).copyTo(destinationFile2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void testCopyFolderInvokesCopyToOnAllFoldersInSourceWithFolderWithSameNameFromDestination() {
|
||||
String folderName1 = "nameOfFolder1";
|
||||
String folderName2 = "nameOfFolder2";
|
||||
Folder folder1 = mock(Folder.class);
|
||||
Folder folder2 = mock(Folder.class);
|
||||
Folder destinationfolder1 = mock(Folder.class);
|
||||
Folder destinationfolder2 = mock(Folder.class);
|
||||
when(source.folders()).thenReturn((Stream) asList(folder1, folder2).stream());
|
||||
when(source.files()).thenReturn(Stream.empty());
|
||||
when(destination.folder(folderName1)).thenReturn(destinationfolder1);
|
||||
when(destination.folder(folderName2)).thenReturn(destinationfolder2);
|
||||
when(folder1.name()).thenReturn(folderName1);
|
||||
when(folder2.name()).thenReturn(folderName2);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
|
||||
verify(folder1).copyTo(destinationfolder1);
|
||||
verify(folder2).copyTo(destinationfolder2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFolderFailsWithIllegalArgumentExceptionIfSourceIsNestedInDestination() {
|
||||
when(source.isAncestorOf(destination)).thenReturn(false);
|
||||
when(destination.isAncestorOf(source)).thenReturn(true);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyFolderFailsWithIllegalArgumentExceptionIfDestinationIsNestedInSource() {
|
||||
when(source.isAncestorOf(destination)).thenReturn(true);
|
||||
when(destination.isAncestorOf(source)).thenReturn(false);
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
|
||||
Copier.copy(source, destination);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.cryptomator.filesystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class PathResolverTest {
|
||||
|
||||
private final Folder root = Mockito.mock(Folder.class);
|
||||
private final Folder foo = Mockito.mock(Folder.class);
|
||||
private final Folder bar = Mockito.mock(Folder.class);
|
||||
private final File baz = Mockito.mock(File.class);
|
||||
|
||||
@Before
|
||||
public void configureMocks() throws IOException {
|
||||
Mockito.doReturn(Optional.empty()).when(root).parent();
|
||||
Mockito.doReturn(Optional.of(root)).when(foo).parent();
|
||||
Mockito.doReturn(Optional.of(foo)).when(bar).parent();
|
||||
|
||||
Mockito.doReturn(foo).when(root).folder("foo");
|
||||
Mockito.doReturn(bar).when(foo).folder("bar");
|
||||
Mockito.doReturn(baz).when(bar).file("baz");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveSameFolder() {
|
||||
Assert.assertEquals(foo, PathResolver.resolveFolder(foo, ""));
|
||||
Assert.assertEquals(foo, PathResolver.resolveFolder(foo, "/"));
|
||||
Assert.assertEquals(foo, PathResolver.resolveFolder(foo, "///"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveChildFolder() {
|
||||
Assert.assertEquals(bar, PathResolver.resolveFolder(root, "foo/bar"));
|
||||
Assert.assertEquals(bar, PathResolver.resolveFolder(root, "foo/./bar"));
|
||||
Assert.assertEquals(bar, PathResolver.resolveFolder(root, "./foo/././bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveParentFolder() {
|
||||
Assert.assertEquals(foo, PathResolver.resolveFolder(bar, ".."));
|
||||
Assert.assertEquals(root, PathResolver.resolveFolder(bar, "../.."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveSiblingFolder() {
|
||||
Assert.assertEquals(foo, PathResolver.resolveFolder(bar, "../../foo"));
|
||||
}
|
||||
|
||||
@Test(expected = UncheckedIOException.class)
|
||||
public void testResolveUnresolvableFolder() {
|
||||
PathResolver.resolveFolder(root, "..");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testResolveFileWithEmptyPath() {
|
||||
PathResolver.resolveFile(root, "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveFile() {
|
||||
Assert.assertEquals(baz, PathResolver.resolveFile(foo, "../foo/bar/./baz"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DelegatingFileSystemTest {
|
||||
|
||||
@Test
|
||||
public void testQuotaAvailableBytes() {
|
||||
FileSystem mockFs = Mockito.mock(FileSystem.class);
|
||||
Mockito.when(mockFs.fileSystem()).thenReturn(mockFs);
|
||||
Mockito.when(mockFs.quotaAvailableBytes()).thenReturn(Optional.of(42l));
|
||||
|
||||
DelegatingFileSystem delegatingFs = TestDelegatingFileSystem.withRoot(mockFs);
|
||||
Assert.assertEquals(mockFs.quotaAvailableBytes(), delegatingFs.quotaAvailableBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuotaUsedBytes() {
|
||||
FileSystem mockFs = Mockito.mock(FileSystem.class);
|
||||
Mockito.when(mockFs.fileSystem()).thenReturn(mockFs);
|
||||
Mockito.when(mockFs.quotaUsedBytes()).thenReturn(Optional.of(23l));
|
||||
|
||||
DelegatingFileSystem delegatingFs = TestDelegatingFileSystem.withRoot(mockFs);
|
||||
Assert.assertEquals(mockFs.quotaUsedBytes(), delegatingFs.quotaUsedBytes());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DelegatingFileTest {
|
||||
|
||||
@Test
|
||||
public void testName() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Mockito.when(mockFile.name()).thenReturn("Test");
|
||||
Assert.assertEquals(mockFile.name(), delegatingFile.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Mockito.when(mockFile.size()).thenReturn(42l);
|
||||
Assert.assertEquals(42l, delegatingFile.size());
|
||||
Mockito.verify(mockFile).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParent() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
|
||||
TestDelegatingFileSystem delegatingParent = TestDelegatingFileSystem.withRoot(mockFolder);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(delegatingParent, mockFile);
|
||||
Assert.assertEquals(delegatingParent, delegatingFile.parent().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExists() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Mockito.when(mockFile.exists()).thenReturn(true);
|
||||
Assert.assertTrue(delegatingFile.exists());
|
||||
|
||||
Mockito.when(mockFile.exists()).thenReturn(false);
|
||||
Assert.assertFalse(delegatingFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLastModified() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Mockito.when(mockFile.lastModified()).thenReturn(now);
|
||||
Assert.assertEquals(now, delegatingFile.lastModified());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLastModified() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Instant now = Instant.now();
|
||||
delegatingFile.setLastModified(now);
|
||||
Mockito.verify(mockFile).setLastModified(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreationTime() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Mockito.when(mockFile.creationTime()).thenReturn(Optional.of(now));
|
||||
Assert.assertEquals(now, delegatingFile.creationTime().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCreationTime() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
Instant now = Instant.now();
|
||||
delegatingFile.setCreationTime(now);
|
||||
Mockito.verify(mockFile).setCreationTime(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenReadable() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
|
||||
Mockito.when(mockFile.openReadable()).thenReturn(mockReadableFile);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
Assert.assertNotNull(delegatingFile.openReadable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenWritable() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
|
||||
Mockito.when(mockFile.openWritable()).thenReturn(mockWritableFile);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
Assert.assertNotNull(delegatingFile.openWritable());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveTo() {
|
||||
File mockFile1 = Mockito.mock(File.class);
|
||||
File mockFile2 = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile1 = new TestDelegatingFile(null, mockFile1);
|
||||
DelegatingFile<?> delegatingFile2 = new TestDelegatingFile(null, mockFile2);
|
||||
|
||||
delegatingFile1.moveTo(delegatingFile2);
|
||||
Mockito.verify(mockFile1).moveTo(mockFile2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testMoveToDestinationFromDifferentLayer() {
|
||||
File mockFile1 = Mockito.mock(File.class);
|
||||
File mockFile2 = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile1 = new TestDelegatingFile(null, mockFile1);
|
||||
|
||||
delegatingFile1.moveTo(mockFile2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyTo() {
|
||||
File mockFile1 = Mockito.mock(File.class);
|
||||
File mockFile2 = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile1 = new TestDelegatingFile(null, mockFile1);
|
||||
DelegatingFile<?> delegatingFile2 = new TestDelegatingFile(null, mockFile2);
|
||||
|
||||
delegatingFile1.copyTo(delegatingFile2);
|
||||
Mockito.verify(mockFile1).copyTo(mockFile2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyToDestinationFromDifferentLayer() {
|
||||
File mockFile1 = Mockito.mock(File.class);
|
||||
File mockFile2 = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile1 = new TestDelegatingFile(null, mockFile1);
|
||||
|
||||
delegatingFile1.copyTo(mockFile2);
|
||||
Mockito.verify(mockFile1).copyTo(mockFile2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
File mockFile = Mockito.mock(File.class);
|
||||
DelegatingFile<?> delegatingFile = new TestDelegatingFile(null, mockFile);
|
||||
|
||||
delegatingFile.delete();
|
||||
Mockito.verify(mockFile).delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareTo() {
|
||||
File mockFile1 = Mockito.mock(File.class);
|
||||
File mockFile2 = Mockito.mock(File.class);
|
||||
|
||||
Mockito.when(mockFile1.compareTo(mockFile2)).thenReturn(-1);
|
||||
DelegatingFile<?> delegatingFile1 = new TestDelegatingFile(null, mockFile1);
|
||||
DelegatingFile<?> delegatingFile2 = new TestDelegatingFile(null, mockFile2);
|
||||
Assert.assertEquals(-1, delegatingFile1.compareTo(delegatingFile2));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DelegatingFolderTest {
|
||||
|
||||
@Test
|
||||
public void testName() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Mockito.when(mockFolder.name()).thenReturn("Test");
|
||||
Assert.assertEquals(mockFolder.name(), delegatingFolder.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParent() {
|
||||
Folder mockFolder1 = Mockito.mock(Folder.class);
|
||||
Folder mockFolder2 = Mockito.mock(Folder.class);
|
||||
|
||||
TestDelegatingFileSystem delegatingParent = TestDelegatingFileSystem.withRoot(mockFolder1);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(delegatingParent, mockFolder2);
|
||||
Assert.assertEquals(delegatingParent, delegatingFolder.parent().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExists() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Mockito.when(mockFolder.exists()).thenReturn(true);
|
||||
Assert.assertTrue(delegatingFolder.exists());
|
||||
|
||||
Mockito.when(mockFolder.exists()).thenReturn(false);
|
||||
Assert.assertFalse(delegatingFolder.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLastModified() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Mockito.when(mockFolder.lastModified()).thenReturn(now);
|
||||
Assert.assertEquals(now, delegatingFolder.lastModified());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLastModified() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Instant now = Instant.now();
|
||||
delegatingFolder.setLastModified(now);
|
||||
Mockito.verify(mockFolder).setLastModified(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreationTime() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Instant now = Instant.now();
|
||||
Mockito.when(mockFolder.creationTime()).thenReturn(Optional.of(now));
|
||||
Assert.assertEquals(now, delegatingFolder.creationTime().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCreationTime() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
Instant now = Instant.now();
|
||||
delegatingFolder.setCreationTime(now);
|
||||
Mockito.verify(mockFolder).setCreationTime(now);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildren() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
TestDelegatingFileSystem delegatingFolder = TestDelegatingFileSystem.withRoot(mockFolder);
|
||||
|
||||
Folder subFolder1 = Mockito.mock(Folder.class);
|
||||
TestDelegatingFolder delegatingSubFolder1 = new TestDelegatingFolder(delegatingFolder, subFolder1);
|
||||
File subFile1 = Mockito.mock(File.class);
|
||||
TestDelegatingFile delegatingSubFile1 = new TestDelegatingFile(delegatingFolder, subFile1);
|
||||
|
||||
/* folders */
|
||||
Mockito.when(mockFolder.folder("subFolder1")).thenReturn(subFolder1);
|
||||
Assert.assertEquals(delegatingSubFolder1, delegatingFolder.folder("subFolder1"));
|
||||
|
||||
Mockito.<Stream<? extends Folder>>when(mockFolder.folders()).thenAnswer((invocation) -> {
|
||||
return Arrays.stream(new Folder[] {subFolder1});
|
||||
});
|
||||
List<TestDelegatingFolder> subFolders = delegatingFolder.folders().collect(Collectors.toList());
|
||||
Assert.assertThat(subFolders, Matchers.containsInAnyOrder(delegatingSubFolder1));
|
||||
|
||||
/* files */
|
||||
Mockito.when(mockFolder.file("subFile1")).thenReturn(subFile1);
|
||||
Assert.assertEquals(delegatingSubFile1, delegatingFolder.file("subFile1"));
|
||||
|
||||
Mockito.<Stream<? extends File>>when(mockFolder.files()).thenAnswer((invocation) -> {
|
||||
return Arrays.stream(new File[] {subFile1});
|
||||
});
|
||||
List<TestDelegatingFile> subFiles = delegatingFolder.files().collect(Collectors.toList());
|
||||
Assert.assertThat(subFiles, Matchers.containsInAnyOrder(delegatingSubFile1));
|
||||
|
||||
/* files and folders */
|
||||
List<Node> children = delegatingFolder.children().collect(Collectors.toList());
|
||||
DelegatingNode<?>[] expectedChildren = new DelegatingNode[] {delegatingSubFolder1, delegatingSubFile1};
|
||||
Assert.assertThat(children, Matchers.containsInAnyOrder(expectedChildren));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMoveTo() {
|
||||
Folder mockFolder1 = Mockito.mock(Folder.class);
|
||||
Folder mockFolder2 = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder1 = new TestDelegatingFolder(null, mockFolder1);
|
||||
DelegatingFolder<?, ?> delegatingFolder2 = new TestDelegatingFolder(null, mockFolder2);
|
||||
|
||||
delegatingFolder1.moveTo(delegatingFolder2);
|
||||
Mockito.verify(mockFolder1).moveTo(mockFolder2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testMoveToDestinationFromDifferentLayer() {
|
||||
Folder mockFolder1 = Mockito.mock(Folder.class);
|
||||
Folder mockFolder2 = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder1 = new TestDelegatingFolder(null, mockFolder1);
|
||||
|
||||
delegatingFolder1.moveTo(mockFolder2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyTo() {
|
||||
Folder mockFolder1 = Mockito.mock(Folder.class);
|
||||
Folder mockFolder2 = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder1 = new TestDelegatingFolder(null, mockFolder1);
|
||||
DelegatingFolder<?, ?> delegatingFolder2 = new TestDelegatingFolder(null, mockFolder2);
|
||||
|
||||
delegatingFolder1.copyTo(delegatingFolder2);
|
||||
Mockito.verify(mockFolder1).copyTo(mockFolder2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCopyToDestinationFromDifferentLayer() {
|
||||
Folder mockFolder1 = Mockito.mock(Folder.class);
|
||||
Folder mockFolder2 = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder1 = new TestDelegatingFolder(null, mockFolder1);
|
||||
|
||||
delegatingFolder1.copyTo(mockFolder2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
delegatingFolder.create();
|
||||
Mockito.verify(mockFolder).create();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
|
||||
delegatingFolder.delete();
|
||||
Mockito.verify(mockFolder).delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubresourcesAreSameInstance() {
|
||||
Folder mockFolder = Mockito.mock(Folder.class);
|
||||
Folder mockSubFolder = Mockito.mock(Folder.class);
|
||||
File mockSubFile = Mockito.mock(File.class);
|
||||
Mockito.when(mockFolder.folder("mockSubFolder")).thenReturn(mockSubFolder);
|
||||
Mockito.when(mockFolder.file("mockSubFile")).thenReturn(mockSubFile);
|
||||
|
||||
DelegatingFolder<?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
|
||||
Assert.assertSame(delegatingFolder.folder("mockSubFolder"), delegatingFolder.folder("mockSubFolder"));
|
||||
Assert.assertSame(delegatingFolder.file("mockSubFile"), delegatingFolder.file("mockSubFile"));
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DelegatingReadableFileTest {
|
||||
|
||||
@Test
|
||||
public void testIsOpen() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
Mockito.when(mockReadableFile.isOpen()).thenReturn(true);
|
||||
Assert.assertTrue(delegatingReadableFile.isOpen());
|
||||
|
||||
Mockito.when(mockReadableFile.isOpen()).thenReturn(false);
|
||||
Assert.assertFalse(delegatingReadableFile.isOpen());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRead() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(4);
|
||||
Mockito.when(mockReadableFile.read(buf)).thenReturn(4);
|
||||
Assert.assertEquals(4, delegatingReadableFile.read(buf));
|
||||
Mockito.verify(mockReadableFile).read(buf);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPosition() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
delegatingReadableFile.position(42);
|
||||
Mockito.verify(mockReadableFile).position(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClose() {
|
||||
ReadableFile mockReadableFile = Mockito.mock(ReadableFile.class);
|
||||
DelegatingReadableFile delegatingReadableFile = new DelegatingReadableFile(mockReadableFile);
|
||||
|
||||
delegatingReadableFile.close();
|
||||
Mockito.verify(mockReadableFile).close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class DelegatingWritableFileTest {
|
||||
|
||||
@Test
|
||||
public void testIsOpen() {
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingWritableFile delegatingWritableFile = new DelegatingWritableFile(mockWritableFile);
|
||||
|
||||
Mockito.when(mockWritableFile.isOpen()).thenReturn(true);
|
||||
Assert.assertTrue(delegatingWritableFile.isOpen());
|
||||
|
||||
Mockito.when(mockWritableFile.isOpen()).thenReturn(false);
|
||||
Assert.assertFalse(delegatingWritableFile.isOpen());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTruncate() {
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingWritableFile delegatingWritableFile = new DelegatingWritableFile(mockWritableFile);
|
||||
|
||||
delegatingWritableFile.truncate();
|
||||
Mockito.verify(mockWritableFile).truncate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrite() {
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingWritableFile delegatingWritableFile = new DelegatingWritableFile(mockWritableFile);
|
||||
|
||||
ByteBuffer buf = ByteBuffer.allocate(4);
|
||||
Mockito.when(mockWritableFile.write(buf)).thenReturn(4);
|
||||
Assert.assertEquals(4, delegatingWritableFile.write(buf));
|
||||
Mockito.verify(mockWritableFile).write(buf);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPosition() {
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
@SuppressWarnings("resource")
|
||||
DelegatingWritableFile delegatingWritableFile = new DelegatingWritableFile(mockWritableFile);
|
||||
|
||||
delegatingWritableFile.position(42);
|
||||
Mockito.verify(mockWritableFile).position(42);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClose() {
|
||||
WritableFile mockWritableFile = Mockito.mock(WritableFile.class);
|
||||
DelegatingWritableFile delegatingWritableFile = new DelegatingWritableFile(mockWritableFile);
|
||||
|
||||
delegatingWritableFile.close();
|
||||
Mockito.verify(mockWritableFile).close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
|
||||
class TestDelegatingFile extends DelegatingFile<TestDelegatingFolder> {
|
||||
|
||||
public TestDelegatingFile(TestDelegatingFolder parent, File delegate) {
|
||||
super(parent, delegate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
class TestDelegatingFileSystem extends TestDelegatingFolder implements DelegatingFileSystem {
|
||||
|
||||
private TestDelegatingFileSystem(Folder delegate) {
|
||||
super(null, delegate);
|
||||
}
|
||||
|
||||
public static TestDelegatingFileSystem withRoot(Folder delegate) {
|
||||
return new TestDelegatingFileSystem(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Folder getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.cryptomator.filesystem.delegating;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
class TestDelegatingFolder extends DelegatingFolder<TestDelegatingFolder, TestDelegatingFile> {
|
||||
|
||||
public TestDelegatingFolder(TestDelegatingFolder parent, Folder delegate) {
|
||||
super(parent, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestDelegatingFile newFile(File delegate) {
|
||||
return new TestDelegatingFile(this, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestDelegatingFolder newFolder(Folder delegate) {
|
||||
return new TestDelegatingFolder(this, delegate);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ByteBuffersTest {
|
||||
|
||||
@Test
|
||||
public void testCopyOfEmptySource() {
|
||||
final ByteBuffer src = ByteBuffer.allocate(0);
|
||||
final ByteBuffer dst = ByteBuffer.allocate(5);
|
||||
dst.put(new byte[3]);
|
||||
Assert.assertEquals(0, src.position());
|
||||
Assert.assertEquals(0, src.remaining());
|
||||
Assert.assertEquals(3, dst.position());
|
||||
Assert.assertEquals(2, dst.remaining());
|
||||
ByteBuffers.copy(src, dst);
|
||||
Assert.assertEquals(0, src.position());
|
||||
Assert.assertEquals(0, src.remaining());
|
||||
Assert.assertEquals(3, dst.position());
|
||||
Assert.assertEquals(2, dst.remaining());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyToEmptyDestination() {
|
||||
final ByteBuffer src = ByteBuffer.wrap(new byte[4]);
|
||||
final ByteBuffer dst = ByteBuffer.allocate(0);
|
||||
src.put(new byte[2]);
|
||||
Assert.assertEquals(2, src.position());
|
||||
Assert.assertEquals(2, src.remaining());
|
||||
Assert.assertEquals(0, dst.position());
|
||||
Assert.assertEquals(0, dst.remaining());
|
||||
ByteBuffers.copy(src, dst);
|
||||
Assert.assertEquals(2, src.position());
|
||||
Assert.assertEquals(2, src.remaining());
|
||||
Assert.assertEquals(0, dst.position());
|
||||
Assert.assertEquals(0, dst.remaining());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyToBiggerDestination() {
|
||||
final ByteBuffer src = ByteBuffer.wrap(new byte[2]);
|
||||
final ByteBuffer dst = ByteBuffer.allocate(10);
|
||||
dst.put(new byte[3]);
|
||||
Assert.assertEquals(0, src.position());
|
||||
Assert.assertEquals(2, src.remaining());
|
||||
Assert.assertEquals(3, dst.position());
|
||||
Assert.assertEquals(7, dst.remaining());
|
||||
ByteBuffers.copy(src, dst);
|
||||
Assert.assertEquals(2, src.position());
|
||||
Assert.assertEquals(0, src.remaining());
|
||||
Assert.assertEquals(5, dst.position());
|
||||
Assert.assertEquals(5, dst.remaining());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCopyToSmallerDestination() {
|
||||
final ByteBuffer src = ByteBuffer.wrap(new byte[5]);
|
||||
final ByteBuffer dst = ByteBuffer.allocate(2);
|
||||
Assert.assertEquals(0, src.position());
|
||||
Assert.assertEquals(5, src.remaining());
|
||||
Assert.assertEquals(0, dst.position());
|
||||
Assert.assertEquals(2, dst.remaining());
|
||||
ByteBuffers.copy(src, dst);
|
||||
Assert.assertEquals(2, src.position());
|
||||
Assert.assertEquals(3, src.remaining());
|
||||
Assert.assertEquals(2, dst.position());
|
||||
Assert.assertEquals(0, dst.remaining());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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<Charset> CHARSETS = Arrays.asList(StandardCharsets.UTF_8, StandardCharsets.US_ASCII, StandardCharsets.UTF_16);
|
||||
|
||||
@DataPoints
|
||||
public static final Iterable<String> 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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2015 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-charsets</artifactId>
|
||||
<name>Cryptomator filesystem: Charset compatibility layer</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,32 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFile;
|
||||
|
||||
class NormalizedNameFile extends DelegatingFile<NormalizedNameFolder> {
|
||||
|
||||
private final Form displayForm;
|
||||
|
||||
public NormalizedNameFile(NormalizedNameFolder parent, File delegate, Form displayForm) {
|
||||
super(parent, delegate);
|
||||
this.displayForm = displayForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return Normalizer.normalize(super.name(), displayForm);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
|
||||
|
||||
public class NormalizedNameFileSystem extends NormalizedNameFolder implements DelegatingFileSystem {
|
||||
|
||||
public NormalizedNameFileSystem(Folder delegate, Form displayForm) {
|
||||
super(null, delegate, displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Folder getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFolder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class NormalizedNameFolder extends DelegatingFolder<NormalizedNameFolder, NormalizedNameFile> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NormalizedNameFolder.class);
|
||||
private final Form displayForm;
|
||||
|
||||
public NormalizedNameFolder(NormalizedNameFolder parent, Folder delegate, Form displayForm) {
|
||||
super(parent, delegate);
|
||||
this.displayForm = displayForm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() throws UncheckedIOException {
|
||||
return Normalizer.normalize(super.name(), displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NormalizedNameFile file(String name) throws UncheckedIOException {
|
||||
String nfcName = Normalizer.normalize(name, Form.NFC);
|
||||
String nfdName = Normalizer.normalize(name, Form.NFD);
|
||||
NormalizedNameFile nfcFile = super.file(nfcName);
|
||||
NormalizedNameFile nfdFile = super.file(nfdName);
|
||||
if (!nfcName.equals(nfdName) && nfcFile.exists() && nfdFile.exists()) {
|
||||
LOG.debug("Ambiguous file names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||
} else if (!nfcName.equals(nfdName) && !nfcFile.exists() && nfdFile.exists()) {
|
||||
LOG.debug("Moving file from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||
nfdFile.moveTo(nfcFile);
|
||||
}
|
||||
return nfcFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NormalizedNameFile newFile(File delegate) {
|
||||
return new NormalizedNameFile(this, delegate, displayForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NormalizedNameFolder folder(String name) throws UncheckedIOException {
|
||||
String nfcName = Normalizer.normalize(name, Form.NFC);
|
||||
String nfdName = Normalizer.normalize(name, Form.NFD);
|
||||
NormalizedNameFolder nfcFolder = super.folder(nfcName);
|
||||
NormalizedNameFolder nfdFolder = super.folder(nfdName);
|
||||
if (!nfcName.equals(nfdName) && nfcFolder.exists() && nfdFolder.exists()) {
|
||||
LOG.debug("Ambiguous folder names \"" + nfcName + "\" (NFC) vs. \"" + nfdName + "\" (NFD). Both files exist. Using \"" + nfcName + "\" (NFC).");
|
||||
} else if (!nfcName.equals(nfdName) && !nfcFolder.exists() && nfdFolder.exists()) {
|
||||
LOG.debug("Moving folder from \"" + nfcName + "\" (NFD) to \"" + nfdName + "\" (NFC).");
|
||||
nfdFolder.moveTo(nfcFolder);
|
||||
}
|
||||
return nfcFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NormalizedNameFolder newFolder(Folder delegate) {
|
||||
return new NormalizedNameFolder(this, delegate, displayForm);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* Makes sure, the filesystems wrapped by this filesystem work only on UTF-8 encoded file paths using Normalization Form C.
|
||||
* Filesystems wrapping this file system, on the other hand, will get filenames reported in a specified Normalization Form.
|
||||
* It is recommended to use NFD for OS X and NFC for other operating systems.
|
||||
* When looking for a file or folder with a name given in either form, both possibilities are considered
|
||||
* and files/folders stored in NFD are automatically migrated to NFC.
|
||||
*/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class NormalizedNameFileSystemTest {
|
||||
|
||||
@Test
|
||||
public void testFileMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
try (WritableFile writable = inMemoryFs.file("\u006F\u0302").openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
try (WritableFile writable = inMemoryFs.file("\u00F4").openWritable()) {
|
||||
writable.write(ByteBuffer.allocate(0));
|
||||
}
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.file("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.file("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.file("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.file("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFolderMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("\u00F4").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertTrue(normalizationFs.folder("\u00F4").exists());
|
||||
Assert.assertTrue(normalizationFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertFalse(inMemoryFs.folder("\u006F\u0302").exists());
|
||||
Assert.assertTrue(inMemoryFs.folder("\u00F4").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNfcDisplayNames() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("a\u00F4").create();
|
||||
inMemoryFs.folder("b\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFC);
|
||||
Assert.assertEquals("a\u00F4", normalizationFs.folder("a\u00F4").name());
|
||||
Assert.assertEquals("b\u00F4", normalizationFs.folder("b\u006F\u0302").name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNfdDisplayNames() {
|
||||
FileSystem inMemoryFs = new InMemoryFileSystem();
|
||||
inMemoryFs.folder("a\u00F4").create();
|
||||
inMemoryFs.folder("b\u006F\u0302").create();
|
||||
FileSystem normalizationFs = new NormalizedNameFileSystem(inMemoryFs, Form.NFD);
|
||||
Assert.assertEquals("a\u006F\u0302", normalizationFs.folder("a\u00F4").name());
|
||||
Assert.assertEquals("b\u006F\u0302", normalizationFs.folder("b\u006F\u0302").name());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class NormalizedNameFileTest {
|
||||
|
||||
private File delegateNfc;
|
||||
private File delegateNfd;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
delegateNfc = Mockito.mock(File.class);
|
||||
delegateNfd = Mockito.mock(File.class);
|
||||
Mockito.when(delegateNfc.name()).thenReturn("\u00C5");
|
||||
Mockito.when(delegateNfd.name()).thenReturn("\u0041\u030A");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfc() {
|
||||
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFC);
|
||||
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFC);
|
||||
Assert.assertEquals("\u00C5", file1.name());
|
||||
Assert.assertEquals("\u00C5", file2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfd() {
|
||||
File file1 = new NormalizedNameFile(null, delegateNfc, Form.NFD);
|
||||
File file2 = new NormalizedNameFile(null, delegateNfd, Form.NFD);
|
||||
Assert.assertEquals("\u0041\u030A", file1.name());
|
||||
Assert.assertEquals("\u0041\u030A", file2.name());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.charsets;
|
||||
|
||||
import java.text.Normalizer.Form;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class NormalizedNameFolderTest {
|
||||
|
||||
private Folder delegate;
|
||||
private File delegateSubFileNfc;
|
||||
private File delegateSubFileNfd;
|
||||
private Folder delegateSubFolderNfc;
|
||||
private Folder delegateSubFolderNfd;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
delegate = Mockito.mock(Folder.class);
|
||||
delegateSubFileNfc = Mockito.mock(File.class);
|
||||
delegateSubFileNfd = Mockito.mock(File.class);
|
||||
Mockito.when(delegate.file("\u00C5")).thenReturn(delegateSubFileNfc);
|
||||
Mockito.when(delegateSubFileNfc.name()).thenReturn("\u00C5");
|
||||
Mockito.when(delegate.file("\u0041\u030A")).thenReturn(delegateSubFileNfd);
|
||||
Mockito.when(delegateSubFileNfd.name()).thenReturn("\u0041\u030A");
|
||||
delegateSubFolderNfc = Mockito.mock(Folder.class);
|
||||
delegateSubFolderNfd = Mockito.mock(Folder.class);
|
||||
Mockito.when(delegate.folder("\u00F4")).thenReturn(delegateSubFolderNfc);
|
||||
Mockito.when(delegateSubFolderNfc.name()).thenReturn("\u00F4");
|
||||
Mockito.when(delegate.folder("\u006F\u0302")).thenReturn(delegateSubFolderNfd);
|
||||
Mockito.when(delegateSubFolderNfd.name()).thenReturn("\u006F\u0302");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfc() {
|
||||
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFC);
|
||||
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFC);
|
||||
Assert.assertEquals("\u00F4", folder1.name());
|
||||
Assert.assertEquals("\u00F4", folder2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisplayNameInNfd() {
|
||||
Folder folder1 = new NormalizedNameFolder(null, delegateSubFolderNfc, Form.NFD);
|
||||
Folder folder2 = new NormalizedNameFolder(null, delegateSubFolderNfd, Form.NFD);
|
||||
Assert.assertEquals("\u006F\u0302", folder1.name());
|
||||
Assert.assertEquals("\u006F\u0302", folder2.name());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration1() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration2() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFolderMigration3() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Mockito.verify(delegateSubFolderNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFolderMigration() {
|
||||
Mockito.when(delegateSubFolderNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFolderNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
Folder sub1 = folder.folder("\u00F4");
|
||||
Mockito.verify(delegateSubFolderNfd).moveTo(delegateSubFolderNfc);
|
||||
Folder sub2 = folder.folder("\u006F\u0302");
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration1() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration2() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(true);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFileMigration3() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(false);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Mockito.verify(delegateSubFileNfd, Mockito.never()).moveTo(Mockito.any());
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFileMigration() {
|
||||
Mockito.when(delegateSubFileNfc.exists()).thenReturn(false);
|
||||
Mockito.when(delegateSubFileNfd.exists()).thenReturn(true);
|
||||
Folder folder = new NormalizedNameFolder(null, delegate, Form.NFC);
|
||||
File sub1 = folder.file("\u00C5");
|
||||
Mockito.verify(delegateSubFileNfd).moveTo(delegateSubFileNfc);
|
||||
File sub2 = folder.file("\u0041\u030A");
|
||||
Assert.assertSame(sub1, sub2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
@@ -1 +0,0 @@
|
||||
/target/
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2016 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto-integration-tests</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer tests</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-nameshortening</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,33 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
|
||||
|
||||
/**
|
||||
* Used as drop-in-replacement for {@link CryptoEngineModule} during unit tests.
|
||||
*/
|
||||
public class CryptoEngineTestModule extends CryptoEngineModule {
|
||||
|
||||
@Override
|
||||
public SecureRandom provideSecureRandom() {
|
||||
return new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.engine.impl.CryptoEngineModule;
|
||||
import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
/**
|
||||
* To be used in integration tests, where a {@link CryptoFileSystem} is needed in conjunction with {@link CryptoEngineTestModule} (which mocks the CSPRNG) as follows:
|
||||
* <code>
|
||||
* DaggerCryptoFileSystemTestComponent.builder().cryptoEngineModule(new CryptoEngineTestModule()).build()
|
||||
* </code>
|
||||
*/
|
||||
@Singleton
|
||||
@Component(modules = CryptoEngineModule.class)
|
||||
public interface CryptoFileSystemTestComponent {
|
||||
|
||||
CryptoFileSystemFactory cryptoFileSystemFactory();
|
||||
|
||||
ShorteningFileSystemFactory shorteningFileSystemFactory();
|
||||
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.Node;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CryptoFileSystemIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemIntegrationTest.class);
|
||||
|
||||
private final CryptoFileSystemTestComponent cryptoFsComp = DaggerCryptoFileSystemTestComponent.builder().cryptoEngineModule(new CryptoEngineTestModule()).build();
|
||||
|
||||
private CryptoFileSystemDelegate cryptoDelegate;
|
||||
private FileSystem ciphertextFs;
|
||||
private FileSystem cleartextFs;
|
||||
|
||||
@Before
|
||||
public void setupFileSystems() {
|
||||
cryptoDelegate = Mockito.mock(CryptoFileSystemDelegate.class);
|
||||
ciphertextFs = new InMemoryFileSystem();
|
||||
FileSystem shorteningFs = cryptoFsComp.shorteningFileSystemFactory().get(ciphertextFs);
|
||||
cryptoFsComp.cryptoFileSystemFactory().initializeNew(shorteningFs, "TopSecret");
|
||||
cleartextFs = cryptoFsComp.cryptoFileSystemFactory().unlockExisting(shorteningFs, "TopSecret", cryptoDelegate);
|
||||
}
|
||||
|
||||
@Test(timeout = 1000)
|
||||
public void testVaultStructureInitializationAndBackupBehaviour() throws UncheckedIOException, IOException {
|
||||
final FileSystem physicalFs = new InMemoryFileSystem();
|
||||
final File masterkeyFile = physicalFs.file("masterkey.cryptomator");
|
||||
final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
|
||||
final Folder physicalDataRoot = physicalFs.folder("d");
|
||||
Assert.assertFalse(masterkeyFile.exists());
|
||||
Assert.assertFalse(masterkeyBkupFile.exists());
|
||||
Assert.assertFalse(physicalDataRoot.exists());
|
||||
|
||||
cryptoFsComp.cryptoFileSystemFactory().initializeNew(physicalFs, "asd");
|
||||
Assert.assertTrue(masterkeyFile.exists());
|
||||
Assert.assertFalse(masterkeyBkupFile.exists());
|
||||
Assert.assertFalse(physicalDataRoot.exists());
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
final FileSystem cryptoFs = cryptoFsComp.cryptoFileSystemFactory().unlockExisting(physicalFs, "asd", cryptoDelegate);
|
||||
Assert.assertTrue(masterkeyBkupFile.exists());
|
||||
Assert.assertTrue(physicalDataRoot.exists());
|
||||
Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
|
||||
Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfLongFolderNames() {
|
||||
final String shortName = "normal folder name";
|
||||
final String longName = "this will be a long filename after encryption, because its encrypted name is longer than onehundredandeighty characters";
|
||||
|
||||
final Folder shortFolder = cleartextFs.folder(shortName);
|
||||
final Folder longFolder = cleartextFs.folder(longName);
|
||||
|
||||
shortFolder.create();
|
||||
longFolder.create();
|
||||
|
||||
// because of the long file, a metadata folder should exist on the physical layer:
|
||||
Assert.assertEquals(1, ciphertextFs.folder("m").folders().count());
|
||||
Assert.assertTrue(ciphertextFs.folder("m").exists());
|
||||
|
||||
// but the shortened filenames must not be visible on the cleartext layer:
|
||||
Assert.assertArrayEquals(new String[] {shortName, longName}, cleartextFs.folders().map(Node::name).sorted().toArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryptionOfFiles() {
|
||||
// write test content to encrypted file
|
||||
try (WritableFile writable = cleartextFs.file("test1.txt").openWritable()) {
|
||||
writable.write(ByteBuffer.wrap("Hello ".getBytes()));
|
||||
writable.write(ByteBuffer.wrap("World".getBytes()));
|
||||
}
|
||||
|
||||
File physicalFile = ciphertextFs.folder("d").folders().findAny().get().folders().findAny().get().files().findAny().get();
|
||||
Assert.assertTrue(physicalFile.exists());
|
||||
|
||||
// read test content from decrypted file
|
||||
try (ReadableFile readable = cleartextFs.file("test1.txt").openReadable()) {
|
||||
ByteBuffer buf1 = ByteBuffer.allocate(5);
|
||||
readable.read(buf1);
|
||||
buf1.flip();
|
||||
Assert.assertEquals("Hello", new String(buf1.array(), 0, buf1.remaining()));
|
||||
ByteBuffer buf2 = ByteBuffer.allocate(10);
|
||||
readable.read(buf2);
|
||||
buf2.flip();
|
||||
Assert.assertArrayEquals(" World".getBytes(), Arrays.copyOfRange(buf2.array(), 0, buf2.remaining()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForcedDecryptionOfManipulatedFile() {
|
||||
// write test content to encrypted file
|
||||
try (WritableFile writable = cleartextFs.file("test1.txt").openWritable()) {
|
||||
writable.write(ByteBuffer.wrap("Hello World".getBytes()));
|
||||
}
|
||||
|
||||
File physicalFile = ciphertextFs.folder("d").folders().findAny().get().folders().findAny().get().files().findAny().get();
|
||||
Assert.assertTrue(physicalFile.exists());
|
||||
|
||||
// toggle last bit
|
||||
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate((int) physicalFile.size());
|
||||
readable.read(buf);
|
||||
buf.array()[buf.limit() - 1] ^= 0x01;
|
||||
buf.flip();
|
||||
writable.write(buf);
|
||||
}
|
||||
|
||||
// whitelist
|
||||
Mockito.when(cryptoDelegate.shouldSkipAuthentication("/test1.txt")).thenReturn(true);
|
||||
|
||||
// read test content from decrypted file
|
||||
try (ReadableFile readable = cleartextFs.file("test1.txt").openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate(11);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
Assert.assertArrayEquals("Hello World".getBytes(), buf.array());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||
File file = cleartextFs.file("benchmark.test");
|
||||
|
||||
final long encStart = System.nanoTime();
|
||||
try (WritableFile writable = file.openWritable()) {
|
||||
final ByteBuffer cleartext = ByteBuffer.allocate(100000); // 100k
|
||||
for (int i = 0; i < 1000; i++) { // 100M total
|
||||
cleartext.rewind();
|
||||
writable.write(cleartext);
|
||||
}
|
||||
}
|
||||
final long encEnd = System.nanoTime();
|
||||
LOG.debug("Encryption of 100M took {}ms", (encEnd - encStart) / 1000 / 1000);
|
||||
|
||||
final long decStart = System.nanoTime();
|
||||
try (ReadableFile readable = file.openReadable()) {
|
||||
final ByteBuffer cleartext = ByteBuffer.allocate(100000); // 100k
|
||||
for (int i = 0; i < 1000; i++) { // 100M total
|
||||
cleartext.clear();
|
||||
readable.read(cleartext);
|
||||
cleartext.flip();
|
||||
Assert.assertEquals(cleartext.get(), 0x00);
|
||||
}
|
||||
}
|
||||
final long decEnd = System.nanoTime();
|
||||
LOG.debug("Decryption of 100M took {}ms", (decEnd - decStart) / 1000 / 1000);
|
||||
|
||||
file.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRandomAccessOnLastBlock() {
|
||||
// prepare test data:
|
||||
ByteBuffer testData = ByteBuffer.allocate(16000 * Integer.BYTES); // < 64kb
|
||||
for (int i = 0; i < 16000; i++) {
|
||||
testData.putInt(i);
|
||||
}
|
||||
|
||||
// write test data to file:
|
||||
File cleartextFile = cleartextFs.file("test");
|
||||
try (WritableFile writable = cleartextFile.openWritable()) {
|
||||
testData.flip();
|
||||
writable.write(testData);
|
||||
}
|
||||
|
||||
// read last block:
|
||||
try (ReadableFile readable = cleartextFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.clear();
|
||||
readable.position(15999 * Integer.BYTES);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
Assert.assertEquals(15999, buf.getInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSequentialRandomAccess() {
|
||||
// prepare test data:
|
||||
ByteBuffer testData = ByteBuffer.allocate(1_000_000 * Integer.BYTES); // = 4MB
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
testData.putInt(i);
|
||||
}
|
||||
|
||||
// write test data to file:
|
||||
File cleartextFile = cleartextFs.file("test");
|
||||
try (WritableFile writable = cleartextFile.openWritable()) {
|
||||
testData.flip();
|
||||
writable.write(testData);
|
||||
}
|
||||
|
||||
// shuffle our test positions:
|
||||
List<Integer> nums = new ArrayList<>();
|
||||
for (int i = 0; i < 1_000_000; i++) {
|
||||
nums.add(i);
|
||||
}
|
||||
Collections.shuffle(nums);
|
||||
|
||||
// read parts from positions:
|
||||
try (ReadableFile readable = cleartextFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
int num = nums.get(i);
|
||||
buf.clear();
|
||||
readable.position(num * Integer.BYTES);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
Assert.assertEquals(num, buf.getInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParallelRandomAccess() {
|
||||
// prepare test data:
|
||||
ByteBuffer testData = ByteBuffer.allocate(1_000_000 * Integer.BYTES); // = 4MB
|
||||
for (int i = 0; i < 1000000; i++) {
|
||||
testData.putInt(i);
|
||||
}
|
||||
|
||||
// write test data to file:
|
||||
final File cleartextFile = cleartextFs.file("test");
|
||||
try (WritableFile writable = cleartextFile.openWritable()) {
|
||||
testData.flip();
|
||||
writable.write(testData);
|
||||
}
|
||||
|
||||
// shuffle our test positions:
|
||||
List<Integer> nums = new ArrayList<>();
|
||||
for (int i = 0; i < 1_000_000; i++) {
|
||||
nums.add(i);
|
||||
}
|
||||
Collections.shuffle(nums);
|
||||
|
||||
// read parts from positions in parallel:
|
||||
final ForkJoinPool pool = new ForkJoinPool(10);
|
||||
final List<Future<Boolean>> tasks = new ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
final int num = nums.get(i);
|
||||
final ForkJoinTask<Boolean> task = ForkJoinTask.adapt(() -> {
|
||||
try (ReadableFile readable = cleartextFile.openReadable()) {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.clear();
|
||||
readable.position(num * Integer.BYTES);
|
||||
readable.read(buf);
|
||||
buf.flip();
|
||||
int numRead = buf.getInt();
|
||||
return num == numRead;
|
||||
}
|
||||
});
|
||||
pool.execute(task);
|
||||
tasks.add(task);
|
||||
}
|
||||
|
||||
// Wait for tasks to finish and check results
|
||||
Assert.assertTrue(tasks.stream().allMatch(task -> {
|
||||
try {
|
||||
return task.get();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration status="WARN">
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
|
||||
</Console>
|
||||
<Console name="StdErr" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="DEBUG">
|
||||
<AppenderRef ref="Console" />
|
||||
<AppenderRef ref="StdErr" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
||||
1
main/filesystem-crypto/.gitignore
vendored
1
main/filesystem-crypto/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/target/
|
||||
@@ -1,93 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2015 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>filesystem-crypto</artifactId>
|
||||
<name>Cryptomator filesystem: Encryption layer</name>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.51</bouncycastle.version>
|
||||
<sivmode.version>1.2.0</sivmode.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Crypto -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>siv-mode</artifactId>
|
||||
<version>${sivmode.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Commons -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Tests -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>commons-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>filesystem-inmemory</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,29 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
public class AuthenticationFailedException extends CryptoException {
|
||||
|
||||
public AuthenticationFailedException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public AuthenticationFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public AuthenticationFailedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
public abstract class CryptoException extends RuntimeException {
|
||||
|
||||
public CryptoException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public CryptoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CryptoException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CryptoException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
/**
|
||||
* A Cryptor instance, once initialized with a set of keys, provides access to threadsafe cryptographic routines.
|
||||
*/
|
||||
public interface Cryptor extends Destroyable {
|
||||
|
||||
FilenameCryptor getFilenameCryptor();
|
||||
|
||||
FileContentCryptor getFileContentCryptor();
|
||||
|
||||
void randomizeMasterkey();
|
||||
|
||||
void readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException;
|
||||
|
||||
byte[] writeKeysToMasterkeyFile(CharSequence passphrase);
|
||||
|
||||
@Override
|
||||
void destroy();
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Factory for stateful {@link FileContentEncryptor Encryptor}/{@link FileContentDecryptor Decryptor} instances, that are capable of processing data exactly once.
|
||||
*/
|
||||
public interface FileContentCryptor {
|
||||
|
||||
public static final ByteBuffer EOF = ByteBuffer.allocate(0);
|
||||
|
||||
/**
|
||||
* @return The fixed number of bytes of the file header. The header length is implementation-specific.
|
||||
*/
|
||||
int getHeaderSize();
|
||||
|
||||
/**
|
||||
* @return The ciphertext position that correlates to the cleartext position.
|
||||
*/
|
||||
long toCiphertextPos(long cleartextPos);
|
||||
|
||||
/**
|
||||
* @param header The full fixed-length header of an encrypted file. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
|
||||
* @param firstCiphertextByte Position of the first ciphertext byte passed to the decryptor. If the decryptor can not fast-forward to the requested byte, an exception is thrown.
|
||||
* If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the decryptors internal block size, an IllegalArgumentException will be thrown.
|
||||
* @param authenticate Skip authentication by setting this flag to <code>false</code>. Should be <code>true</code> by default.
|
||||
* @return A possibly new FileContentDecryptor instance which is capable of decrypting ciphertexts associated with the given file header.
|
||||
*/
|
||||
FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) throws IllegalArgumentException, AuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* @param header The full fixed-length header of an encrypted file or {@link Optional#empty()}. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
|
||||
* If the header is empty, a new one will be created by the returned encryptor.
|
||||
* @param firstCleartextByte Position of the first cleartext byte passed to the encryptor. If the encryptor can not fast-forward to the requested byte, an exception is thrown.
|
||||
* If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the encryptors internal block size, an IllegalArgumentException will be thrown.
|
||||
* @return A possibly new FileContentEncryptor instance which is capable of encrypting cleartext associated with the given file header.
|
||||
*/
|
||||
FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header, long firstCleartextByte) throws IllegalArgumentException;
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
/**
|
||||
* Stateful, thus not thread-safe.
|
||||
*/
|
||||
public interface FileContentDecryptor extends Destroyable, Closeable {
|
||||
|
||||
/**
|
||||
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
||||
*
|
||||
* @param cleartext Cleartext data or {@link FileContentCryptor#EOF} to indicate the end of a ciphertext.
|
||||
* @see #skipToPosition(long)
|
||||
*/
|
||||
void append(ByteBuffer ciphertext) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Cancels decryption due to an exception in the thread responsible for appending ciphertext.
|
||||
* The exception will be the root cause of an {@link UncheckedIOException} thrown by {@link #cleartext()} when retrieving the decrypted result.
|
||||
*
|
||||
* @param cause The exception making it impossible to {@link #append(ByteBuffer)} further ciphertext.
|
||||
*/
|
||||
void cancelWithException(Exception cause) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Returns the next decrypted cleartext in byte-by-byte FIFO order, meaning in the order ciphertext has been appended to this encryptor.
|
||||
* However the number and size of the cleartext byte buffers doesn't need to resemble the ciphertext buffers.
|
||||
*
|
||||
* This method might block if no cleartext is available yet.
|
||||
*
|
||||
* @return Decrypted cleartext or {@link FileContentCryptor#EOF}.
|
||||
* @throws AuthenticationFailedException On MAC mismatches
|
||||
* @throws UncheckedIOException In case of I/O exceptions, e.g. caused by previous {@link #cancelWithException(Exception)}.
|
||||
*/
|
||||
ByteBuffer cleartext() throws InterruptedException, AuthenticationFailedException, UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Clears file-specific sensitive information.
|
||||
*/
|
||||
@Override
|
||||
void destroy();
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
/**
|
||||
* Stateful, thus not thread-safe.
|
||||
*/
|
||||
public interface FileContentEncryptor extends Destroyable, Closeable {
|
||||
|
||||
/**
|
||||
* Creates the encrypted file header. This header might depend on the already encrypted data,
|
||||
* thus the caller should make sure all data is processed before requesting the header.
|
||||
*
|
||||
* @return Encrypted file header.
|
||||
*/
|
||||
ByteBuffer getHeader();
|
||||
|
||||
/**
|
||||
* @return the size of headers created by this {@code FileContentCryptor}. The length of headers returned by {@link #getHeader()} equals this value.
|
||||
*/
|
||||
int getHeaderSize();
|
||||
|
||||
/**
|
||||
* Appends further cleartext to this encryptor. This method might block until space becomes available.
|
||||
*
|
||||
* @param cleartext Cleartext data or {@link FileContentCryptor#EOF} to indicate the end of a cleartext.
|
||||
*/
|
||||
void append(ByteBuffer cleartext) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Cancels encryption due to an exception in the thread responsible for appending cleartext.
|
||||
* The exception will be the root cause of an {@link UncheckedIOException} thrown by {@link #ciphertext()} when retrieving the encrypted result.
|
||||
*
|
||||
* @param cause The exception making it impossible to {@link #append(ByteBuffer)} further cleartext.
|
||||
*/
|
||||
void cancelWithException(Exception cause) throws InterruptedException;
|
||||
|
||||
/**
|
||||
* Returns the next ciphertext in byte-by-byte FIFO order, meaning in the order cleartext has been appended to this encryptor.
|
||||
* However the number and size of the ciphertext byte buffers doesn't need to resemble the cleartext buffers.
|
||||
*
|
||||
* This method might block if no ciphertext is available yet.
|
||||
*
|
||||
* @return Encrypted ciphertext of {@link FileContentCryptor#EOF}.
|
||||
* @throws UncheckedIOException In case of I/O exceptions, e.g. caused by previous {@link #cancelWithException(Exception)}.
|
||||
*/
|
||||
ByteBuffer ciphertext() throws InterruptedException, UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Clears file-specific sensitive information.
|
||||
*/
|
||||
@Override
|
||||
void destroy();
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts,
|
||||
* otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Deterministic_encryption">Wikipedia on deterministic encryption</a>
|
||||
*/
|
||||
public interface FilenameCryptor {
|
||||
|
||||
/**
|
||||
* @return constant length string, that is unlikely to collide with any other name.
|
||||
*/
|
||||
String hashDirectoryId(String cleartextDirectoryId);
|
||||
|
||||
/**
|
||||
* @return A Pattern that can be used to test, if a name is a well-formed ciphertext.
|
||||
*/
|
||||
Pattern encryptedNamePattern();
|
||||
|
||||
/**
|
||||
* @param cleartextName original filename including cleartext file extension
|
||||
* @param associatedData optional associated data, that will not get encrypted but needs to be provided during decryption
|
||||
* @return encrypted filename without any file extension
|
||||
*/
|
||||
String encryptFilename(String cleartextName, byte[]... associatedData);
|
||||
|
||||
/**
|
||||
* @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first.
|
||||
* @param associatedData the same associated data used during encryption, otherwise and {@link AuthenticationFailedException} will be thrown
|
||||
* @return cleartext filename, probably including its cleartext file extension.
|
||||
*/
|
||||
String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
public class InvalidPassphraseException extends CryptoException {
|
||||
|
||||
public InvalidPassphraseException() {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
public class UnsupportedVaultFormatException extends CryptoException {
|
||||
|
||||
private final Integer detectedVersion;
|
||||
private final Integer latestSupportedVersion;
|
||||
|
||||
public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
|
||||
this.detectedVersion = detectedVersion;
|
||||
this.latestSupportedVersion = latestSupportedVersion;
|
||||
}
|
||||
|
||||
public Integer getDetectedVersion() {
|
||||
return detectedVersion;
|
||||
}
|
||||
|
||||
public Integer getLatestSupportedVersion() {
|
||||
return latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isVaultOlderThanSoftware() {
|
||||
return detectedVersion == null || detectedVersion < latestSupportedVersion;
|
||||
}
|
||||
|
||||
public boolean isSoftwareOlderThanVault() {
|
||||
return detectedVersion > latestSupportedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
final class AesKeyWrap {
|
||||
|
||||
private static final String RFC3394_CIPHER = "AESWrap";
|
||||
|
||||
private AesKeyWrap() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param kek Key encrypting key
|
||||
* @param key Key to be wrapped
|
||||
* @return Wrapped key
|
||||
*/
|
||||
public static byte[] wrap(SecretKey kek, SecretKey key) {
|
||||
final Cipher cipher;
|
||||
try {
|
||||
cipher = Cipher.getInstance(RFC3394_CIPHER);
|
||||
cipher.init(Cipher.WRAP_MODE, kek);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("Invalid key.", e);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException("Algorithm/Padding should exist.", e);
|
||||
}
|
||||
|
||||
try {
|
||||
return cipher.wrap(key);
|
||||
} catch (InvalidKeyException | IllegalBlockSizeException e) {
|
||||
throw new IllegalStateException("Unable to wrap key.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param kek Key encrypting key
|
||||
* @param wrappedKey Key to be unwrapped
|
||||
* @param keyAlgorithm Key designation, i.e. algorithm name to be associated with the unwrapped key.
|
||||
* @return Unwrapped key
|
||||
* @throws NoSuchAlgorithmException If keyAlgorithm is unknown
|
||||
* @throws InvalidKeyException If unwrapping failed (i.e. wrong kek)
|
||||
*/
|
||||
public static SecretKey unwrap(SecretKey kek, byte[] wrappedKey, String keyAlgorithm) throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
final Cipher cipher;
|
||||
try {
|
||||
cipher = Cipher.getInstance(RFC3394_CIPHER);
|
||||
cipher.init(Cipher.UNWRAP_MODE, kek);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException("Invalid key.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
||||
throw new IllegalStateException("Algorithm/Padding should exist.", ex);
|
||||
}
|
||||
|
||||
return (SecretKey) cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||
|
||||
public static final int PAYLOAD_SIZE = 32 * 1024;
|
||||
public static final int NONCE_SIZE = 16;
|
||||
public static final int MAC_SIZE = 32;
|
||||
public static final int CHUNK_SIZE = NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE;
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class CryptoEngineModule {
|
||||
|
||||
@Provides
|
||||
public Cryptor provideCryptor(SecureRandom secureRandom) {
|
||||
return new CryptorImpl(secureRandom);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public SecureRandom provideSecureRandom() {
|
||||
try {
|
||||
// https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/
|
||||
final SecureRandom nativeRandom = SecureRandom.getInstanceStrong();
|
||||
byte[] seed = nativeRandom.generateSeed(55); // NIST SP800-90A suggests 440 bits for SHA1 seed
|
||||
SecureRandom sha1Random = SecureRandom.getInstance("SHA1PRNG");
|
||||
sha1Random.setSeed(seed);
|
||||
return sha1Random;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("No strong PRNGs available.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.cryptomator.common.LazyInitializer;
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.cryptomator.crypto.engine.InvalidPassphraseException;
|
||||
import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
|
||||
|
||||
class CryptorImpl implements Cryptor {
|
||||
|
||||
private static final int SCRYPT_SALT_LENGTH = 8;
|
||||
private static final int SCRYPT_COST_PARAM = 1 << 14;
|
||||
private static final int SCRYPT_BLOCK_SIZE = 8;
|
||||
private static final int KEYLENGTH_IN_BYTES = 32;
|
||||
private static final String ENCRYPTION_ALG = "AES";
|
||||
private static final String MAC_ALG = "HmacSHA256";
|
||||
|
||||
private SecretKey encryptionKey;
|
||||
private SecretKey macKey;
|
||||
private final AtomicReference<FilenameCryptor> filenameCryptor = new AtomicReference<>();
|
||||
private final AtomicReference<FileContentCryptor> fileContentCryptor = new AtomicReference<>();
|
||||
private final SecureRandom randomSource;
|
||||
|
||||
public CryptorImpl(SecureRandom randomSource) {
|
||||
this.randomSource = randomSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilenameCryptor getFilenameCryptor() {
|
||||
assertKeysExist();
|
||||
return LazyInitializer.initializeLazily(filenameCryptor, () -> {
|
||||
return new FilenameCryptorImpl(encryptionKey, macKey);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileContentCryptor getFileContentCryptor() {
|
||||
assertKeysExist();
|
||||
return LazyInitializer.initializeLazily(fileContentCryptor, () -> {
|
||||
return new FileContentCryptorImpl(encryptionKey, macKey, randomSource);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertKeysExist() {
|
||||
if (encryptionKey == null || encryptionKey.isDestroyed()) {
|
||||
throw new IllegalStateException("No or invalid encryptionKey.");
|
||||
}
|
||||
if (macKey == null || macKey.isDestroyed()) {
|
||||
throw new IllegalStateException("No or invalid MAC key.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void randomizeMasterkey() {
|
||||
try {
|
||||
KeyGenerator encKeyGen = KeyGenerator.getInstance(ENCRYPTION_ALG);
|
||||
encKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
encryptionKey = encKeyGen.generateKey();
|
||||
KeyGenerator macKeyGen = KeyGenerator.getInstance(MAC_ALG);
|
||||
macKeyGen.init(KEYLENGTH_IN_BYTES * Byte.SIZE, randomSource);
|
||||
macKey = macKeyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
|
||||
final KeyFile keyFile;
|
||||
try {
|
||||
final ObjectMapper om = new ObjectMapper();
|
||||
keyFile = om.readValue(masterkeyFileContents, KeyFile.class);
|
||||
if (keyFile == null) {
|
||||
throw new InvalidFormatException("Could not read masterkey file", null, KeyFile.class);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Unable to parse masterkeyFileContents", e);
|
||||
}
|
||||
assert keyFile != null;
|
||||
|
||||
// check version
|
||||
if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
|
||||
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
|
||||
}
|
||||
|
||||
final byte[] kekBytes = Scrypt.scrypt(passphrase, keyFile.getScryptSalt(), keyFile.getScryptCostParam(), keyFile.getScryptBlockSize(), KEYLENGTH_IN_BYTES);
|
||||
try {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
|
||||
// future use (as soon as we need to prevent downgrade attacks):
|
||||
// final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
// final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
// if (!MessageDigest.isEqual(versionMac, keyFile.getVersionMac())) {
|
||||
// destroyQuietly(macKey);
|
||||
// throw new UnsupportedVaultFormatException(Integer.MAX_VALUE, CURRENT_VAULT_VERSION);
|
||||
// }
|
||||
this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidPassphraseException();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
} finally {
|
||||
Arrays.fill(kekBytes, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] writeKeysToMasterkeyFile(CharSequence passphrase) {
|
||||
final byte[] scryptSalt = new byte[SCRYPT_SALT_LENGTH];
|
||||
randomSource.nextBytes(scryptSalt);
|
||||
|
||||
final byte[] kekBytes = Scrypt.scrypt(passphrase, scryptSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, KEYLENGTH_IN_BYTES);
|
||||
final byte[] wrappedEncryptionKey;
|
||||
final byte[] wrappedMacKey;
|
||||
try {
|
||||
final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
|
||||
wrappedEncryptionKey = AesKeyWrap.wrap(kek, encryptionKey);
|
||||
wrappedMacKey = AesKeyWrap.wrap(kek, macKey);
|
||||
} finally {
|
||||
Arrays.fill(kekBytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
final Mac mac = new ThreadLocalMac(macKey, MAC_ALG).get();
|
||||
final byte[] versionMac = mac.doFinal(ByteBuffer.allocate(Integer.BYTES).putInt(CURRENT_VAULT_VERSION).array());
|
||||
|
||||
final KeyFile keyfile = new KeyFile();
|
||||
keyfile.setVersion(CURRENT_VAULT_VERSION);
|
||||
keyfile.setScryptSalt(scryptSalt);
|
||||
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
|
||||
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
|
||||
keyfile.setEncryptionMasterKey(wrappedEncryptionKey);
|
||||
keyfile.setMacMasterKey(wrappedMacKey);
|
||||
keyfile.setVersionMac(versionMac);
|
||||
|
||||
try {
|
||||
final ObjectMapper om = new ObjectMapper();
|
||||
return om.writeValueAsBytes(keyfile);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Unable to create JSON from " + keyfile, e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================= destruction ======================= */
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
destroyQuietly(encryptionKey);
|
||||
destroyQuietly(macKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return (encryptionKey == null || encryptionKey.isDestroyed()) && (macKey == null || macKey.isDestroyed());
|
||||
}
|
||||
|
||||
private void destroyQuietly(Destroyable d) {
|
||||
if (d == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
d.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* Executes long-running computations and returns the result strictly in order of the job submissions, no matter how long each job takes.
|
||||
*
|
||||
* The internally used thread pool is shut down automatically as soon as this FifiParallelDataProcessor is no longer referenced (see Finalization behaviour of {@link ThreadPoolExecutor}).
|
||||
*/
|
||||
class FifoParallelDataProcessor<T> {
|
||||
|
||||
private final BlockingQueue<Future<T>> processedData;
|
||||
private final ExecutorService executorService;
|
||||
|
||||
/**
|
||||
* @param numThreads How many jobs can run in parallel.
|
||||
* @param workAhead Maximum number of jobs accepted in {@link #submit(Callable)} without blocking until results are polled from {@link #processedData()}.
|
||||
*/
|
||||
public FifoParallelDataProcessor(int workAhead, ExecutorService executorService) {
|
||||
this.processedData = new ArrayBlockingQueue<>(workAhead);
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a job for execution. The results of multiple submissions can be polled in FIFO order using {@link #processedData()}.
|
||||
*
|
||||
* @param processingJob A task, that will compute a result.
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
void submit(Callable<T> processingJob) throws InterruptedException {
|
||||
Future<T> future = executorService.submit(processingJob);
|
||||
processedData.put(future);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits already pre-processed data, that can be polled in FIFO order from {@link #processedData()}.
|
||||
*
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
void submitPreprocessed(T preprocessedData) throws InterruptedException {
|
||||
this.submit(() -> {
|
||||
return preprocessedData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of previously {@link #submit(Callable) submitted} jobs in the same order as they have been submitted. Blocks if the job didn't finish yet.
|
||||
*
|
||||
* @return Next job result
|
||||
* @throws InterruptedException If the calling thread was interrupted while waiting for the next result.
|
||||
*/
|
||||
T processedData() throws InterruptedException, ExecutionException {
|
||||
return processedData.take().get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentDecryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentEncryptor;
|
||||
|
||||
class FileContentCryptorImpl implements FileContentCryptor {
|
||||
|
||||
private final SecretKey encryptionKey;
|
||||
private final SecretKey macKey;
|
||||
private final SecureRandom randomSource;
|
||||
|
||||
FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey, SecureRandom randomSource) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
this.randomSource = randomSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeaderSize() {
|
||||
return FileHeader.HEADER_SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long toCiphertextPos(long cleartextPos) {
|
||||
long chunkNum = cleartextPos / PAYLOAD_SIZE;
|
||||
long cleartextChunkStart = chunkNum * PAYLOAD_SIZE;
|
||||
assert cleartextChunkStart <= cleartextPos;
|
||||
long chunkInternalDiff = cleartextPos - cleartextChunkStart;
|
||||
assert chunkInternalDiff >= 0 && chunkInternalDiff < PAYLOAD_SIZE;
|
||||
long ciphertextChunkStart = chunkNum * CHUNK_SIZE;
|
||||
return ciphertextChunkStart + chunkInternalDiff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) throws IllegalArgumentException, AuthenticationFailedException {
|
||||
if (header.remaining() != getHeaderSize()) {
|
||||
throw new IllegalArgumentException("Invalid header.");
|
||||
}
|
||||
if (firstCiphertextByte % CHUNK_SIZE != 0) {
|
||||
throw new IllegalArgumentException("Invalid starting point for decryption.");
|
||||
}
|
||||
return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header, long firstCleartextByte) {
|
||||
if (header.isPresent() && header.get().remaining() != getHeaderSize()) {
|
||||
throw new IllegalArgumentException("Invalid header.");
|
||||
}
|
||||
if (firstCleartextByte % PAYLOAD_SIZE != 0) {
|
||||
throw new IllegalArgumentException("Invalid starting point for encryption.");
|
||||
}
|
||||
return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource, firstCleartextByte);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.MAC_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.NONCE_SIZE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentDecryptor;
|
||||
import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
|
||||
private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS + READ_AHEAD, SHARED_DECRYPTION_EXECUTOR);
|
||||
private final Supplier<Mac> hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final boolean authenticate;
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
|
||||
this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
|
||||
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
||||
this.authenticate = authenticate;
|
||||
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
||||
|
||||
// vault version 5 and onwards should have filesize: -1
|
||||
if (this.header.getPayload().getFilesize() != -1l) {
|
||||
throw new UncheckedIOException(new IOException("Attempted to decrypt file with invalid header (probably from previous vault version)"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(ByteBuffer ciphertext) throws InterruptedException {
|
||||
if (ciphertext == FileContentCryptor.EOF) {
|
||||
submitCiphertextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
while (ciphertext.hasRemaining()) {
|
||||
ByteBuffers.copy(ciphertext, ciphertextBuffer);
|
||||
submitCiphertextBufferIfFull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelWithException(Exception cause) throws InterruptedException {
|
||||
dataProcessor.submit(() -> {
|
||||
throw cause;
|
||||
});
|
||||
}
|
||||
|
||||
private void submitCiphertextBufferIfFull() throws InterruptedException {
|
||||
if (!ciphertextBuffer.hasRemaining()) {
|
||||
submitCiphertextBuffer();
|
||||
ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitCiphertextBuffer() throws InterruptedException {
|
||||
ciphertextBuffer.flip();
|
||||
if (ciphertextBuffer.hasRemaining()) {
|
||||
Callable<ByteBuffer> encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++);
|
||||
dataProcessor.submit(encryptionJob);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitEof() throws InterruptedException {
|
||||
dataProcessor.submitPreprocessed(FileContentCryptor.EOF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer cleartext() throws InterruptedException {
|
||||
try {
|
||||
return dataProcessor.processedData();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||
throw new AuthenticationFailedException(e);
|
||||
} else if (e.getCause() instanceof IOException || e.getCause() instanceof UncheckedIOException) {
|
||||
throw new UncheckedIOException(new IOException("Decryption failed due to I/O exception during ciphertext supply.", e));
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
header.destroy();
|
||||
}
|
||||
|
||||
private class DecryptionJob implements Callable<ByteBuffer> {
|
||||
|
||||
private final byte[] nonce;
|
||||
private final ByteBuffer inBuf;
|
||||
private final ByteBuffer chunkNumberBigEndian = ByteBuffer.allocate(Long.BYTES);
|
||||
private final byte[] expectedMac;
|
||||
|
||||
public DecryptionJob(ByteBuffer ciphertextChunk, long chunkNumber) {
|
||||
if (ciphertextChunk.remaining() < NONCE_SIZE + MAC_SIZE) {
|
||||
throw new IllegalArgumentException("Chunk must at least contain a NONCE and a MAC");
|
||||
}
|
||||
this.nonce = new byte[NONCE_SIZE];
|
||||
ByteBuffer nonceBuf = ciphertextChunk.asReadOnlyBuffer();
|
||||
nonceBuf.position(0).limit(NONCE_SIZE);
|
||||
nonceBuf.get(nonce);
|
||||
this.inBuf = ciphertextChunk.asReadOnlyBuffer();
|
||||
this.inBuf.position(NONCE_SIZE).limit(ciphertextChunk.limit() - MAC_SIZE);
|
||||
chunkNumberBigEndian.putLong(chunkNumber);
|
||||
chunkNumberBigEndian.rewind();
|
||||
this.expectedMac = new byte[MAC_SIZE];
|
||||
ByteBuffer macBuf = ciphertextChunk.asReadOnlyBuffer();
|
||||
macBuf.position(macBuf.limit() - MAC_SIZE);
|
||||
macBuf.get(expectedMac);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer call() {
|
||||
try {
|
||||
if (authenticate) {
|
||||
Mac mac = hmacSha256.get();
|
||||
mac.update(header.getIv());
|
||||
mac.update(chunkNumberBigEndian.asReadOnlyBuffer());
|
||||
mac.update(nonce);
|
||||
mac.update(inBuf.asReadOnlyBuffer());
|
||||
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
|
||||
chunkNumberBigEndian.rewind();
|
||||
throw new AuthenticationFailedException("Auth error in chunk " + chunkNumberBigEndian.getLong());
|
||||
}
|
||||
}
|
||||
|
||||
Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
cipher.init(Cipher.DECRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonce));
|
||||
ByteBuffer outBuf = ByteBuffer.allocate(cipher.getOutputSize(inBuf.remaining()));
|
||||
cipher.update(inBuf, outBuf);
|
||||
outBuf.flip();
|
||||
return outBuf;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalStateException("File content key created by current class invalid.", e);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enought.", e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException("CTR mode known to accept an IV (aka. nonce).", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.NONCE_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentEncryptor;
|
||||
import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
|
||||
private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS + READ_AHEAD, SHARED_DECRYPTION_EXECUTOR);
|
||||
private final ThreadLocalMac hmacSha256;
|
||||
private final SecretKey headerKey;
|
||||
private final FileHeader header;
|
||||
private final SecureRandom randomSource;
|
||||
private final LongAdder cleartextBytesScheduledForEncryption = new LongAdder();
|
||||
private ByteBuffer cleartextBuffer = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
public FileContentEncryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom randomSource, long firstCleartextByte) {
|
||||
if (firstCleartextByte != 0) {
|
||||
throw new UnsupportedOperationException("Partial encryption not supported.");
|
||||
}
|
||||
this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
|
||||
this.headerKey = headerKey;
|
||||
this.header = new FileHeader(randomSource);
|
||||
this.randomSource = randomSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
header.getPayload().setFilesize(-1l);
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeaderSize() {
|
||||
return FileHeader.HEADER_SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||
if (cleartext == FileContentCryptor.EOF) {
|
||||
submitCleartextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
appendAllAndSubmitIfFull(cleartext);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
submitCleartextBufferIfFull();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelWithException(Exception cause) throws InterruptedException {
|
||||
dataProcessor.submit(() -> {
|
||||
throw cause;
|
||||
});
|
||||
}
|
||||
|
||||
private void submitCleartextBufferIfFull() throws InterruptedException {
|
||||
if (!cleartextBuffer.hasRemaining()) {
|
||||
submitCleartextBuffer();
|
||||
cleartextBuffer = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitCleartextBuffer() throws InterruptedException {
|
||||
cleartextBuffer.flip();
|
||||
if (cleartextBuffer.hasRemaining()) {
|
||||
Callable<ByteBuffer> encryptionJob = new EncryptionJob(cleartextBuffer, chunkNumber++);
|
||||
dataProcessor.submit(encryptionJob);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitEof() throws InterruptedException {
|
||||
dataProcessor.submitPreprocessed(FileContentCryptor.EOF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer ciphertext() throws InterruptedException {
|
||||
try {
|
||||
return dataProcessor.processedData();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof IOException || e.getCause() instanceof UncheckedIOException) {
|
||||
throw new UncheckedIOException(new IOException("Encryption failed due to I/O exception during cleartext supply.", e));
|
||||
} else {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
header.destroy();
|
||||
}
|
||||
|
||||
private class EncryptionJob implements Callable<ByteBuffer> {
|
||||
|
||||
private final ByteBuffer inBuf;
|
||||
private final ByteBuffer chunkNumberBigEndian = ByteBuffer.allocate(Long.BYTES);
|
||||
|
||||
public EncryptionJob(ByteBuffer cleartextChunk, long chunkNumber) {
|
||||
this.inBuf = cleartextChunk;
|
||||
chunkNumberBigEndian.putLong(chunkNumber);
|
||||
chunkNumberBigEndian.rewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer call() {
|
||||
try {
|
||||
final Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
final Mac mac = hmacSha256.get();
|
||||
final ByteBuffer outBuf = ByteBuffer.allocate(NONCE_SIZE + inBuf.remaining() + mac.getMacLength());
|
||||
|
||||
// nonce
|
||||
byte[] nonce = new byte[NONCE_SIZE];
|
||||
randomSource.nextBytes(nonce);
|
||||
outBuf.put(nonce);
|
||||
|
||||
// payload:
|
||||
cipher.init(Cipher.ENCRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonce));
|
||||
assert cipher.getOutputSize(inBuf.remaining()) == inBuf.remaining() : "input length should be equal to output length in CTR mode.";
|
||||
int bytesEncrypted = cipher.update(inBuf, outBuf);
|
||||
|
||||
// mac:
|
||||
ByteBuffer ciphertextBuf = outBuf.asReadOnlyBuffer();
|
||||
ciphertextBuf.position(NONCE_SIZE).limit(NONCE_SIZE + bytesEncrypted);
|
||||
mac.update(header.getIv());
|
||||
mac.update(chunkNumberBigEndian.asReadOnlyBuffer());
|
||||
mac.update(nonce);
|
||||
mac.update(ciphertextBuf);
|
||||
byte[] authenticationCode = mac.doFinal();
|
||||
outBuf.put(authenticationCode);
|
||||
|
||||
// flip and return:
|
||||
outBuf.flip();
|
||||
return outBuf;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalStateException("File content key created by current class invalid.", e);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new IllegalStateException("Buffer allocated for reported output size apparently not big enought.", e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException("CTR mode known to accept an IV (aka. nonce).", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||
|
||||
class FileHeader implements Destroyable {
|
||||
|
||||
static final int HEADER_SIZE = 88;
|
||||
|
||||
private static final int IV_POS = 0;
|
||||
private static final int IV_LEN = 16;
|
||||
private static final int PAYLOAD_POS = 16;
|
||||
private static final int PAYLOAD_LEN = 40;
|
||||
private static final int MAC_POS = 56;
|
||||
private static final int MAC_LEN = 32;
|
||||
|
||||
private final byte[] iv;
|
||||
private final FileHeaderPayload payload;
|
||||
|
||||
public FileHeader(SecureRandom randomSource) {
|
||||
this.iv = new byte[IV_LEN];
|
||||
this.payload = new FileHeaderPayload(randomSource);
|
||||
randomSource.nextBytes(iv);
|
||||
}
|
||||
|
||||
private FileHeader(byte[] iv, FileHeaderPayload payload) {
|
||||
this.iv = iv;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public FileHeaderPayload getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public ByteBuffer toByteBuffer(SecretKey headerKey, Supplier<Mac> hmacSha256Factory) {
|
||||
ByteBuffer result = ByteBuffer.allocate(HEADER_SIZE);
|
||||
result.position(IV_POS).limit(IV_POS + IV_LEN);
|
||||
result.put(iv);
|
||||
result.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN);
|
||||
result.put(payload.toCiphertextByteBuffer(headerKey, iv));
|
||||
ByteBuffer resultSoFar = result.asReadOnlyBuffer();
|
||||
resultSoFar.flip();
|
||||
Mac mac = hmacSha256Factory.get();
|
||||
assert mac.getMacLength() == MAC_LEN;
|
||||
mac.update(resultSoFar);
|
||||
result.position(MAC_POS).limit(MAC_POS + MAC_LEN);
|
||||
result.put(mac.doFinal());
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return payload.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
payload.destroy();
|
||||
}
|
||||
|
||||
public static FileHeader decrypt(SecretKey headerKey, Supplier<Mac> hmacSha256Factory, ByteBuffer header) throws IllegalArgumentException, AuthenticationFailedException {
|
||||
if (header.remaining() != HEADER_SIZE) {
|
||||
throw new IllegalArgumentException("Invalid header size.");
|
||||
}
|
||||
|
||||
checkHeaderMac(header, hmacSha256Factory.get());
|
||||
|
||||
final byte[] iv = new byte[IV_LEN];
|
||||
final ByteBuffer ivBuf = header.asReadOnlyBuffer();
|
||||
ivBuf.position(IV_POS).limit(IV_POS + IV_LEN);
|
||||
ivBuf.get(iv);
|
||||
|
||||
final ByteBuffer payloadBuf = header.asReadOnlyBuffer();
|
||||
payloadBuf.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN);
|
||||
|
||||
final FileHeaderPayload payload = FileHeaderPayload.fromCiphertextByteBuffer(payloadBuf, headerKey, iv);
|
||||
|
||||
return new FileHeader(iv, payload);
|
||||
}
|
||||
|
||||
private static void checkHeaderMac(ByteBuffer header, Mac mac) throws AuthenticationFailedException {
|
||||
assert mac.getMacLength() == MAC_LEN;
|
||||
ByteBuffer headerData = header.asReadOnlyBuffer();
|
||||
headerData.position(0).limit(MAC_POS);
|
||||
mac.update(headerData);
|
||||
ByteBuffer headerMac = header.asReadOnlyBuffer();
|
||||
headerMac.position(MAC_POS).limit(MAC_POS + MAC_LEN);
|
||||
byte[] expectedMac = new byte[MAC_LEN];
|
||||
headerMac.get(expectedMac);
|
||||
|
||||
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
|
||||
throw new AuthenticationFailedException("Corrupt header.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
class FileHeaderPayload implements Destroyable {
|
||||
|
||||
private static final int FILESIZE_POS = 0;
|
||||
private static final int FILESIZE_LEN = Long.BYTES;
|
||||
private static final int CONTENT_KEY_POS = 8;
|
||||
private static final int CONTENT_KEY_LEN = 32;
|
||||
private static final String AES = "AES";
|
||||
|
||||
private long filesize;
|
||||
private final SecretKey contentKey;
|
||||
|
||||
public FileHeaderPayload(SecureRandom randomSource) {
|
||||
this.filesize = 0;
|
||||
try {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance(AES);
|
||||
keyGen.init(CONTENT_KEY_LEN * Byte.SIZE, randomSource);
|
||||
this.contentKey = keyGen.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private FileHeaderPayload(long filesize, SecretKey contentKey) {
|
||||
this.filesize = filesize;
|
||||
this.contentKey = contentKey;
|
||||
}
|
||||
|
||||
public long getFilesize() {
|
||||
return filesize;
|
||||
}
|
||||
|
||||
public void setFilesize(long filesize) {
|
||||
this.filesize = filesize;
|
||||
}
|
||||
|
||||
public SecretKey getContentKey() {
|
||||
return contentKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return contentKey.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
contentKey.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
private ByteBuffer toCleartextByteBuffer() {
|
||||
ByteBuffer cleartext = ByteBuffer.allocate(FILESIZE_LEN + CONTENT_KEY_LEN);
|
||||
cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN);
|
||||
cleartext.putLong(filesize);
|
||||
cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN);
|
||||
cleartext.put(contentKey.getEncoded());
|
||||
cleartext.flip();
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
public ByteBuffer toCiphertextByteBuffer(SecretKey headerKey, byte[] iv) {
|
||||
final ByteBuffer cleartext = toCleartextByteBuffer();
|
||||
try {
|
||||
Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
cipher.init(Cipher.ENCRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int ciphertextLength = cipher.getOutputSize(cleartext.remaining());
|
||||
assert ciphertextLength == cleartext.remaining() : "in counter mode outputlength == input length";
|
||||
final ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
|
||||
cipher.doFinal(cleartext, ciphertext);
|
||||
ciphertext.flip();
|
||||
return ciphertext;
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to compute encrypted header.", e);
|
||||
} finally {
|
||||
Arrays.fill(cleartext.array(), (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
public static FileHeaderPayload fromCiphertextByteBuffer(ByteBuffer ciphertextPayload, SecretKey headerKey, byte[] iv) {
|
||||
final ByteBuffer cleartext = decryptPayload(ciphertextPayload, headerKey, iv);
|
||||
try {
|
||||
return fromCleartextByteBuffer(cleartext);
|
||||
} finally {
|
||||
// destroy evidence:
|
||||
Arrays.fill(cleartext.array(), (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private static FileHeaderPayload fromCleartextByteBuffer(ByteBuffer cleartext) {
|
||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
||||
try {
|
||||
cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN);
|
||||
final long filesize = cleartext.getLong();
|
||||
cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN);
|
||||
cleartext.get(contentKey);
|
||||
return new FileHeaderPayload(filesize, new SecretKeySpec(contentKey, AES));
|
||||
} finally {
|
||||
// destroy evidence:
|
||||
Arrays.fill(contentKey, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer decryptPayload(ByteBuffer ciphertext, SecretKey headerKey, byte[] iv) {
|
||||
try {
|
||||
Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
cipher.init(Cipher.DECRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int cleartextLength = cipher.getOutputSize(ciphertext.remaining());
|
||||
assert cleartextLength == ciphertext.remaining() : "in counter mode outputlength == input length";
|
||||
final ByteBuffer cleartext = ByteBuffer.allocate(cleartextLength);
|
||||
cipher.doFinal(ciphertext, cleartext);
|
||||
cleartext.flip();
|
||||
return cleartext;
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to decrypt header.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.cryptomator.crypto.engine.AuthenticationFailedException;
|
||||
import org.cryptomator.crypto.engine.FilenameCryptor;
|
||||
import org.cryptomator.siv.SivMode;
|
||||
import org.cryptomator.siv.UnauthenticCiphertextException;
|
||||
|
||||
class FilenameCryptorImpl implements FilenameCryptor {
|
||||
|
||||
private static final BaseNCodec BASE32 = new Base32();
|
||||
// https://tools.ietf.org/html/rfc4648#section-6
|
||||
private static final Pattern BASE32_PATTERN = Pattern.compile("^([A-Z2-7]{8})*[A-Z2-7=]{8}");
|
||||
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
|
||||
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
|
||||
@Override
|
||||
protected SivMode initialValue() {
|
||||
return new SivMode();
|
||||
};
|
||||
};
|
||||
|
||||
private final SecretKey encryptionKey;
|
||||
private final SecretKey macKey;
|
||||
|
||||
FilenameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hashDirectoryId(String cleartextDirectoryId) {
|
||||
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
|
||||
byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes);
|
||||
final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
|
||||
return BASE32.encodeAsString(hashedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pattern encryptedNamePattern() {
|
||||
return BASE32_PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName, byte[]... associatedData) {
|
||||
final byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
|
||||
final byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
|
||||
return BASE32.encodeAsString(encryptedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
|
||||
final byte[] encryptedBytes = BASE32.decode(ciphertextName);
|
||||
try {
|
||||
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
|
||||
return new String(cleartextBytes, UTF_8);
|
||||
} catch (UnauthenticCiphertextException | IllegalBlockSizeException e) {
|
||||
throw new AuthenticationFailedException("Invalid ciphertext.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThreadLocalSha1 extends ThreadLocal<MessageDigest> {
|
||||
|
||||
@Override
|
||||
protected MessageDigest initialValue() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("SHA-1 exists in every JVM");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageDigest get() {
|
||||
final MessageDigest messageDigest = super.get();
|
||||
messageDigest.reset();
|
||||
return messageDigest;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "primaryMasterKey", "hmacMasterKey", "versionMac"})
|
||||
class KeyFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
|
||||
@JsonProperty("version")
|
||||
private Integer version;
|
||||
|
||||
@JsonProperty("scryptSalt")
|
||||
private byte[] scryptSalt;
|
||||
|
||||
@JsonProperty("scryptCostParam")
|
||||
private int scryptCostParam;
|
||||
|
||||
@JsonProperty("scryptBlockSize")
|
||||
private int scryptBlockSize;
|
||||
|
||||
@JsonProperty("primaryMasterKey")
|
||||
private byte[] encryptionMasterKey;
|
||||
|
||||
@JsonProperty("hmacMasterKey")
|
||||
private byte[] macMasterKey;
|
||||
|
||||
@JsonProperty("versionMac")
|
||||
private byte[] versionMac;
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public byte[] getScryptSalt() {
|
||||
return scryptSalt;
|
||||
}
|
||||
|
||||
public void setScryptSalt(byte[] scryptSalt) {
|
||||
this.scryptSalt = scryptSalt;
|
||||
}
|
||||
|
||||
public int getScryptCostParam() {
|
||||
return scryptCostParam;
|
||||
}
|
||||
|
||||
public void setScryptCostParam(int scryptCostParam) {
|
||||
this.scryptCostParam = scryptCostParam;
|
||||
}
|
||||
|
||||
public int getScryptBlockSize() {
|
||||
return scryptBlockSize;
|
||||
}
|
||||
|
||||
public void setScryptBlockSize(int scryptBlockSize) {
|
||||
this.scryptBlockSize = scryptBlockSize;
|
||||
}
|
||||
|
||||
public byte[] getEncryptionMasterKey() {
|
||||
return encryptionMasterKey;
|
||||
}
|
||||
|
||||
public void setEncryptionMasterKey(byte[] encryptionMasterKey) {
|
||||
this.encryptionMasterKey = encryptionMasterKey;
|
||||
}
|
||||
|
||||
public byte[] getMacMasterKey() {
|
||||
return macMasterKey;
|
||||
}
|
||||
|
||||
public void setMacMasterKey(byte[] macMasterKey) {
|
||||
this.macMasterKey = macMasterKey;
|
||||
}
|
||||
|
||||
public byte[] getVersionMac() {
|
||||
return versionMac;
|
||||
}
|
||||
|
||||
public void setVersionMac(byte[] versionMac) {
|
||||
this.versionMac = versionMac;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
|
||||
final class Scrypt {
|
||||
|
||||
private Scrypt() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a key from the given passphrase.
|
||||
* This implementation makes sure, any copies of the passphrase used during key derivation are overwritten in memory asap (before next GC cycle).
|
||||
*
|
||||
* @param passphrase The passphrase
|
||||
* @param salt Salt, ideally randomly generated
|
||||
* @param costParam Cost parameter <code>N</code>, larger than 1, a power of 2 and less than <code>2^(128 * blockSize / 8)</code>
|
||||
* @param blockSize Block size <code>r</code>
|
||||
* @param keyLengthInBytes Key output length <code>dkLen</code>
|
||||
* @return Derived key
|
||||
* @see <a href="https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-04#section-2">RFC Draft</a>
|
||||
*/
|
||||
public static byte[] scrypt(CharSequence passphrase, byte[] salt, int costParam, int blockSize, int keyLengthInBytes) {
|
||||
// This is an attempt to get the password bytes without copies of the password being created in some dark places inside the JVM:
|
||||
final ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
|
||||
final byte[] pw = new byte[buf.remaining()];
|
||||
buf.get(pw);
|
||||
try {
|
||||
return SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBytes);
|
||||
} finally {
|
||||
Arrays.fill(pw, (byte) 0); // overwrite bytes
|
||||
buf.rewind(); // just resets markers
|
||||
buf.put(pw); // this is where we overwrite the actual bytes
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
final class ThreadLocalAesCtrCipher {
|
||||
|
||||
private ThreadLocalAesCtrCipher() {
|
||||
}
|
||||
|
||||
private static final String AES_CTR = "AES/CTR/NoPadding";
|
||||
private static final ThreadLocal<Cipher> THREAD_LOCAL_CIPHER = ThreadLocal.withInitial(ThreadLocalAesCtrCipher::newCipherInstance);
|
||||
|
||||
private static Cipher newCipherInstance() {
|
||||
try {
|
||||
return Cipher.getInstance(AES_CTR);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException("Could not create Cipher.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Cipher get() {
|
||||
return THREAD_LOCAL_CIPHER.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
class ThreadLocalMac extends ThreadLocal<Mac>implements Supplier<Mac> {
|
||||
|
||||
private final SecretKey macKey;
|
||||
private final String macAlgorithm;
|
||||
|
||||
ThreadLocalMac(SecretKey macKey, String macAlgorithm) {
|
||||
this.macKey = macKey;
|
||||
this.macAlgorithm = macAlgorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mac initialValue() {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(macAlgorithm);
|
||||
mac.init(macKey);
|
||||
return mac;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new IllegalStateException("Could not create MAC.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mac get() {
|
||||
Mac mac = super.get();
|
||||
mac.reset();
|
||||
return mac;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
/**
|
||||
* This is where the actual encryption, decryption, hashing and authenticating takes place.
|
||||
*/
|
||||
package org.cryptomator.crypto.engine;
|
||||
@@ -1,35 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFile;
|
||||
|
||||
class BlockAlignedFile extends DelegatingFile<BlockAlignedFolder> {
|
||||
|
||||
private final int blockSize;
|
||||
|
||||
public BlockAlignedFile(BlockAlignedFolder parent, File delegate, int blockSize) {
|
||||
super(parent, delegate);
|
||||
this.blockSize = blockSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockAlignedReadableFile openReadable() throws UncheckedIOException {
|
||||
return new BlockAlignedReadableFile(delegate.openReadable(), blockSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockAlignedWritableFile openWritable() throws UncheckedIOException {
|
||||
return new BlockAlignedWritableFile(delegate::openWritable, delegate::openReadable, blockSize);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFileSystem;
|
||||
|
||||
/**
|
||||
* Provides a decoration layer for the {@link org.cryptomator.filesystem Filesystem API}, which guarantees, that all read/write attempts to underlying files always begin at a block start position.
|
||||
* Block start positions are integer multiples of a block size + a fixed block shift.
|
||||
* <p>
|
||||
* In general the formula to align a requested read with a physical read is <code>floor(x / blockSize) * blockSize</code><br/>
|
||||
* For example <code>blockSize=10</code> result in the following block-aligned read/write attempts:
|
||||
*
|
||||
* <table>
|
||||
* <thead>
|
||||
* <tr>
|
||||
* <th>Requested Read</th>
|
||||
* <th>Physical Read</th>
|
||||
* </tr>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr>
|
||||
* <td>0</td>
|
||||
* <td>0</td></td>
|
||||
* <tr>
|
||||
* <td>5</td>
|
||||
* <td>0</td></td>
|
||||
* <tr>
|
||||
* <td>9</td>
|
||||
* <td>0</td></td>
|
||||
* <tr>
|
||||
* <td>10</td>
|
||||
* <td>10</td></td>
|
||||
* <tr>
|
||||
* <td>11</td>
|
||||
* <td>10</td></td>
|
||||
* <tr>
|
||||
* <td>35</td>
|
||||
* <td>30</td></td>
|
||||
* </tbody>
|
||||
* </table>
|
||||
*/
|
||||
class BlockAlignedFileSystem extends BlockAlignedFolder implements DelegatingFileSystem {
|
||||
|
||||
public BlockAlignedFileSystem(Folder delegate, int blockSize) {
|
||||
super(null, delegate, blockSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Folder getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
@Singleton
|
||||
public class BlockAlignedFileSystemFactory {
|
||||
|
||||
@Inject
|
||||
public BlockAlignedFileSystemFactory() {
|
||||
}
|
||||
|
||||
public FileSystem get(Folder root) {
|
||||
return new BlockAlignedFileSystem(root, PAYLOAD_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.delegating.DelegatingFolder;
|
||||
|
||||
class BlockAlignedFolder extends DelegatingFolder<BlockAlignedFolder, BlockAlignedFile> {
|
||||
|
||||
private final int blockSize;
|
||||
|
||||
public BlockAlignedFolder(BlockAlignedFolder parent, Folder delegate, int blockSize) {
|
||||
super(parent, delegate);
|
||||
this.blockSize = blockSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockAlignedFile newFile(File delegate) {
|
||||
return new BlockAlignedFile(this, delegate, blockSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockAlignedFolder newFolder(Folder delegate) {
|
||||
return new BlockAlignedFolder(this, delegate, blockSize);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class BlockAlignedReadableFile implements ReadableFile {
|
||||
|
||||
private final ReadableFile delegate;
|
||||
private final int blockSize;
|
||||
private final ByteBuffer currentBlockBuffer;
|
||||
private boolean eofReached = false;
|
||||
private Mode mode = Mode.PASSTHROUGH;
|
||||
|
||||
private enum Mode {
|
||||
BLOCK_ALIGNED, PASSTHROUGH;
|
||||
}
|
||||
|
||||
public BlockAlignedReadableFile(ReadableFile delegate, int blockSize) {
|
||||
if (blockSize < 1) {
|
||||
throw new IllegalArgumentException("Invalid block size");
|
||||
}
|
||||
this.delegate = delegate;
|
||||
this.blockSize = blockSize;
|
||||
this.currentBlockBuffer = ByteBuffer.allocate(blockSize);
|
||||
this.currentBlockBuffer.flip(); // so remaining() is 0 -> next read will read from physical source.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long logicalPosition) throws UncheckedIOException {
|
||||
switchToBlockAlignedMode();
|
||||
long blockNumber = logicalPosition / blockSize;
|
||||
long physicalPosition = blockNumber * blockSize;
|
||||
assert physicalPosition <= logicalPosition;
|
||||
int diff = (int) (logicalPosition - physicalPosition);
|
||||
assert diff >= 0;
|
||||
assert diff < blockSize;
|
||||
delegate.position(physicalPosition);
|
||||
eofReached = false;
|
||||
readCurrentBlock();
|
||||
currentBlockBuffer.position(diff);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
void switchToBlockAlignedMode() {
|
||||
mode = Mode.BLOCK_ALIGNED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer target) throws UncheckedIOException {
|
||||
switch (mode) {
|
||||
case PASSTHROUGH:
|
||||
return delegate.read(target);
|
||||
case BLOCK_ALIGNED:
|
||||
return readBlockAligned(target);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported mode " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
private int readBlockAligned(ByteBuffer target) {
|
||||
if (eofReached) {
|
||||
return -1;
|
||||
} else {
|
||||
int read = 0;
|
||||
while (!eofReached && target.hasRemaining()) {
|
||||
read += ByteBuffers.copy(currentBlockBuffer, target);
|
||||
readCurrentBlockIfNeeded();
|
||||
}
|
||||
return read;
|
||||
}
|
||||
}
|
||||
|
||||
private void readCurrentBlockIfNeeded() {
|
||||
if (!currentBlockBuffer.hasRemaining()) {
|
||||
readCurrentBlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void readCurrentBlock() {
|
||||
currentBlockBuffer.clear();
|
||||
if (delegate.read(currentBlockBuffer) == -1) {
|
||||
eofReached = true;
|
||||
}
|
||||
currentBlockBuffer.flip();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return delegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.io.ByteBuffers;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class BlockAlignedWritableFile implements WritableFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BlockAlignedWritableFile.class);
|
||||
|
||||
private final Supplier<WritableFile> openWritable;
|
||||
private final Supplier<ReadableFile> openReadable;
|
||||
private final int blockSize;
|
||||
private final ByteBuffer currentBlockBuffer;
|
||||
private Mode mode = Mode.PASSTHROUGH;
|
||||
private Optional<WritableFile> delegate;
|
||||
private long logicalPosition;
|
||||
|
||||
private enum Mode {
|
||||
BLOCK_ALIGNED, PASSTHROUGH;
|
||||
}
|
||||
|
||||
public BlockAlignedWritableFile(Supplier<WritableFile> openWritable, Supplier<ReadableFile> openReadable, int blockSize) {
|
||||
this.openWritable = openWritable;
|
||||
this.openReadable = openReadable;
|
||||
this.blockSize = blockSize;
|
||||
this.currentBlockBuffer = ByteBuffer.allocate(blockSize);
|
||||
currentBlockBuffer.flip(); // make sure the buffer has no remaining bytes by default
|
||||
delegate = Optional.of(openWritable.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long logicalPosition) throws UncheckedIOException {
|
||||
switchToBlockAlignedMode();
|
||||
this.logicalPosition = logicalPosition;
|
||||
readCurrentBlock();
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
void switchToBlockAlignedMode() {
|
||||
LOG.trace("switching to blockaligend write...");
|
||||
mode = Mode.BLOCK_ALIGNED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer source) throws UncheckedIOException {
|
||||
switch (mode) {
|
||||
case PASSTHROUGH:
|
||||
return delegate.get().write(source);
|
||||
case BLOCK_ALIGNED:
|
||||
return writeBlockAligned(source);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported mode " + mode);
|
||||
}
|
||||
}
|
||||
|
||||
private int writeBlockAligned(ByteBuffer source) {
|
||||
int writtenTotal = 0;
|
||||
while (source.hasRemaining()) {
|
||||
int written = ByteBuffers.copy(source, currentBlockBuffer);
|
||||
logicalPosition += written;
|
||||
writeCurrentBlockIfNeeded();
|
||||
writtenTotal += written;
|
||||
}
|
||||
return writtenTotal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws UncheckedIOException {
|
||||
currentBlockBuffer.flip();
|
||||
writeCurrentBlock();
|
||||
delegate.ifPresent(WritableFile::close);
|
||||
}
|
||||
|
||||
private void writeCurrentBlockIfNeeded() {
|
||||
if (!currentBlockBuffer.hasRemaining()) {
|
||||
writeCurrentBlock();
|
||||
readCurrentBlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCurrentBlock() {
|
||||
currentBlockBuffer.rewind();
|
||||
delegate.get().write(currentBlockBuffer);
|
||||
}
|
||||
|
||||
private void readCurrentBlock() {
|
||||
// TODO lock that shit
|
||||
|
||||
// determine right position:
|
||||
long blockNumber = logicalPosition / blockSize;
|
||||
long physicalPosition = blockNumber * blockSize;
|
||||
|
||||
// switch from write to read access:
|
||||
delegate.get().close();
|
||||
currentBlockBuffer.clear();
|
||||
try (ReadableFile r = openReadable.get()) {
|
||||
r.position(physicalPosition);
|
||||
int numRead = r.read(currentBlockBuffer);
|
||||
assert numRead == currentBlockBuffer.position();
|
||||
}
|
||||
int advance = (int) (logicalPosition - physicalPosition);
|
||||
currentBlockBuffer.position(advance);
|
||||
|
||||
// continue write access:
|
||||
WritableFile w = openWritable.get();
|
||||
w.position(physicalPosition);
|
||||
delegate = Optional.of(w);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return delegate.get().isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void truncate() throws UncheckedIOException {
|
||||
delegate.get().truncate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentDecryptor;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
|
||||
class CiphertextReader implements Callable<Void> {
|
||||
|
||||
private static final int READ_BUFFER_SIZE = 32 * 1024 + 32; // aligned with encrypted chunk size + MAC size
|
||||
|
||||
private final ReadableFile file;
|
||||
private final FileContentDecryptor decryptor;
|
||||
private final long startpos;
|
||||
|
||||
public CiphertextReader(ReadableFile file, FileContentDecryptor decryptor, long startpos) {
|
||||
this.file = file;
|
||||
this.decryptor = decryptor;
|
||||
this.startpos = startpos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws InterruptedIOException {
|
||||
try {
|
||||
callInterruptibly();
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void callInterruptibly() throws InterruptedException {
|
||||
try {
|
||||
file.position(startpos);
|
||||
int bytesRead = -1;
|
||||
do {
|
||||
ByteBuffer ciphertext = ByteBuffer.allocate(READ_BUFFER_SIZE);
|
||||
bytesRead = file.read(ciphertext);
|
||||
if (bytesRead > 0) {
|
||||
ciphertext.flip();
|
||||
assert bytesRead == ciphertext.remaining();
|
||||
decryptor.append(ciphertext);
|
||||
}
|
||||
} while (bytesRead > 0);
|
||||
decryptor.append(FileContentCryptor.EOF);
|
||||
} catch (UncheckedIOException e) {
|
||||
decryptor.cancelWithException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
import org.cryptomator.crypto.engine.FileContentEncryptor;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
class CiphertextWriter implements Callable<Void> {
|
||||
|
||||
private final WritableFile file;
|
||||
private final FileContentEncryptor encryptor;
|
||||
|
||||
public CiphertextWriter(WritableFile file, FileContentEncryptor encryptor) {
|
||||
this.file = file;
|
||||
this.encryptor = encryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws InterruptedIOException {
|
||||
try {
|
||||
callInterruptibly();
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void callInterruptibly() throws InterruptedException {
|
||||
try {
|
||||
ByteBuffer ciphertext;
|
||||
while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
file.write(ciphertext);
|
||||
}
|
||||
} catch (UncheckedIOException e) {
|
||||
encryptor.cancelWithException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class ConflictResolver {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ConflictResolver.class);
|
||||
private static final int UUID_FIRST_GROUP_STRLEN = 8;
|
||||
private static final int MAX_DIR_FILE_SIZE = 87; // "normal" file header has 88 bytes
|
||||
|
||||
private final Pattern encryptedNamePattern;
|
||||
private final Function<String, Optional<String>> nameDecryptor;
|
||||
private final Function<String, Optional<String>> nameEncryptor;
|
||||
|
||||
public ConflictResolver(Pattern encryptedNamePattern, Function<String, Optional<String>> nameDecryptor, Function<String, Optional<String>> nameEncryptor) {
|
||||
this.encryptedNamePattern = encryptedNamePattern;
|
||||
this.nameDecryptor = nameDecryptor;
|
||||
this.nameEncryptor = nameEncryptor;
|
||||
}
|
||||
|
||||
public File resolveIfNecessary(File file) {
|
||||
Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
|
||||
if (m.matches()) {
|
||||
// full match, use file as is
|
||||
return file;
|
||||
} else if (m.find(0)) {
|
||||
// partial match, might be conflicting
|
||||
return resolveConflict(file, m.toMatchResult());
|
||||
} else {
|
||||
// no match, file not relevant
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
|
||||
String ciphertext = matchResult.group();
|
||||
boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
|
||||
Optional<String> cleartext = nameDecryptor.apply(ciphertext);
|
||||
if (cleartext.isPresent()) {
|
||||
Folder folder = conflictingFile.parent().get();
|
||||
File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
|
||||
if (isDirectory && canonicalFile.exists() && isSameFileBasedOnSample(canonicalFile, conflictingFile, MAX_DIR_FILE_SIZE)) {
|
||||
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
|
||||
conflictingFile.delete();
|
||||
return canonicalFile;
|
||||
} else {
|
||||
// conventional conflict detected! look for an alternative name:
|
||||
File alternativeFile;
|
||||
String conflictId;
|
||||
do {
|
||||
conflictId = createConflictId();
|
||||
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
|
||||
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
|
||||
alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
|
||||
} while (alternativeFile.exists());
|
||||
LOG.debug("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
|
||||
conflictingFile.moveTo(alternativeFile);
|
||||
return alternativeFile;
|
||||
}
|
||||
} else {
|
||||
// not decryptable; false positive
|
||||
return conflictingFile;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSameFileBasedOnSample(File file1, File file2, int sampleSize) {
|
||||
if (file1.size() != file2.size()) {
|
||||
return false;
|
||||
} else {
|
||||
try (ReadableFile r1 = file1.openReadable(); ReadableFile r2 = file2.openReadable()) {
|
||||
ByteBuffer beginOfFile1 = ByteBuffer.allocate(sampleSize);
|
||||
ByteBuffer beginOfFile2 = ByteBuffer.allocate(sampleSize);
|
||||
int bytesRead1 = r1.read(beginOfFile1);
|
||||
int bytesRead2 = r2.read(beginOfFile2);
|
||||
if (bytesRead1 == bytesRead2) {
|
||||
beginOfFile1.flip();
|
||||
beginOfFile2.flip();
|
||||
return beginOfFile1.equals(beginOfFile2);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String createConflictId() {
|
||||
return UUID.randomUUID().toString().substring(0, UUID_FIRST_GROUP_STRLEN);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
static final String DATA_ROOT_DIR = "d";
|
||||
static final String ROOT_DIRECOTRY_ID = "";
|
||||
|
||||
public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
|
||||
|
||||
static final String DIR_PREFIX = "0";
|
||||
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.ReadableFile;
|
||||
import org.cryptomator.filesystem.WritableFile;
|
||||
|
||||
class CryptoFile extends CryptoNode implements File {
|
||||
|
||||
public CryptoFile(CryptoFolder parent, String name, Cryptor cryptor) {
|
||||
super(parent, name, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<String> encryptedName() {
|
||||
return parent().get().encryptChildName(name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
if (!physicalFile().isPresent()) {
|
||||
return -1l;
|
||||
} else {
|
||||
File file = physicalFile().get();
|
||||
long ciphertextSize = file.size() - cryptor.getFileContentCryptor().getHeaderSize();
|
||||
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
|
||||
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
|
||||
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
|
||||
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
|
||||
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
|
||||
}
|
||||
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
|
||||
assert additionalCleartextBytes >= 0;
|
||||
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableFile openReadable() {
|
||||
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
|
||||
ReadableFile physicalReadable = forceGetPhysicalFile().openReadable();
|
||||
boolean success = false;
|
||||
try {
|
||||
final ReadableFile result = new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalReadable, authenticate, this::reportAuthError);
|
||||
success = true;
|
||||
return result;
|
||||
} finally {
|
||||
if (!success) {
|
||||
physicalReadable.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reportAuthError() {
|
||||
fileSystem().delegate().authenticationFailed(this.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WritableFile openWritable() {
|
||||
if (parent.folder(name).exists()) {
|
||||
throw new UncheckedIOException(new FileAlreadyExistsException(toString()));
|
||||
}
|
||||
WritableFile physicalWrtiable = forceGetPhysicalFile().openWritable();
|
||||
boolean success = false;
|
||||
try {
|
||||
final WritableFile result = new CryptoWritableFile(cryptor.getFileContentCryptor(), physicalWrtiable);
|
||||
success = true;
|
||||
return result;
|
||||
} finally {
|
||||
if (!success) {
|
||||
physicalWrtiable.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parent.toString() + name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(File o) {
|
||||
return toString().compareTo(o.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() throws UncheckedIOException {
|
||||
forceGetPhysicalFile().delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(File destination) throws UncheckedIOException {
|
||||
if (destination instanceof CryptoFile) {
|
||||
CryptoFile dst = (CryptoFile) destination;
|
||||
forceGetPhysicalFile().moveTo(dst.forceGetPhysicalFile());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can not move CryptoFile to conventional File.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2015, 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.filesystem.crypto.Constants.DATA_ROOT_DIR;
|
||||
import static org.cryptomator.filesystem.crypto.Constants.ROOT_DIRECOTRY_ID;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.cryptomator.crypto.engine.Cryptor;
|
||||
import org.cryptomator.crypto.engine.InvalidPassphraseException;
|
||||
import org.cryptomator.filesystem.File;
|
||||
import org.cryptomator.filesystem.FileSystem;
|
||||
import org.cryptomator.filesystem.Folder;
|
||||
|
||||
class CryptoFileSystem extends CryptoFolder implements FileSystem {
|
||||
|
||||
private final Folder physicalRoot;
|
||||
private final CryptoFileSystemDelegate delegate;
|
||||
|
||||
public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CryptoFileSystemDelegate delegate, CharSequence passphrase) throws InvalidPassphraseException {
|
||||
super(null, "", cryptor);
|
||||
if (cryptor.isDestroyed()) {
|
||||
throw new IllegalArgumentException("Cryptor's keys must not be destroyed.");
|
||||
}
|
||||
this.physicalRoot = physicalRoot;
|
||||
this.delegate = delegate;
|
||||
create();
|
||||
}
|
||||
|
||||
CryptoFileSystemDelegate delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<String> getDirectoryId() {
|
||||
return Optional.of(ROOT_DIRECOTRY_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<File> physicalFile() {
|
||||
throw new UnsupportedOperationException("Crypto filesystem root doesn't provide a directory file, as the directory ID is fixed.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Folder physicalDataRoot() {
|
||||
return physicalRoot.folder(DATA_ROOT_DIR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> quotaUsedBytes() {
|
||||
return physicalRoot.fileSystem().quotaUsedBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Long> quotaAvailableBytes() {
|
||||
return physicalRoot.fileSystem().quotaAvailableBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CryptoFolder> parent() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return forceGetPhysicalFolder().exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Instant> creationTime() throws UncheckedIOException {
|
||||
return forceGetPhysicalFolder().creationTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant lastModified() {
|
||||
return forceGetPhysicalFolder().lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
throw new UnsupportedOperationException("Can not delete CryptoFileSytem root.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveTo(Folder target) {
|
||||
throw new UnsupportedOperationException("Can not move CryptoFileSytem root.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create() {
|
||||
forceGetPhysicalFolder().create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "/";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016 Sebastian Stenzel and others.
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
public interface CryptoFileSystemDelegate {
|
||||
|
||||
/**
|
||||
* Reports the path for resources, that could not be decrypted due to authentication errors.
|
||||
*
|
||||
* @param cleartextPath Unix-style vault-relative path
|
||||
*/
|
||||
void authenticationFailed(String cleartextPath);
|
||||
|
||||
/**
|
||||
* Allows the delegate to deactivate authentication during decryption.
|
||||
* This bears the risk of CCAs, thus this method should only return <code>true</code> for data recovery purposes.
|
||||
*
|
||||
* @param cleartextPath Unix-style vault-relative path
|
||||
* @return Must always <b>default to <code>false</code></b>, except when authentication should be skipped.
|
||||
*/
|
||||
boolean shouldSkipAuthentication(String cleartextPath);
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user