Merge branch 'release/1.3.0-rc1'

# Conflicts:
#	main/ant-kit/pom.xml
#	main/commons-test/pom.xml
#	main/commons/pom.xml
#	main/filesystem-charsets/pom.xml
#	main/filesystem-crypto-integration-tests/pom.xml
#	main/filesystem-crypto/pom.xml
#	main/filesystem-inmemory/pom.xml
#	main/filesystem-invariants-tests/pom.xml
#	main/filesystem-nameshortening/pom.xml
#	main/filesystem-nio/pom.xml
#	main/filesystem-stats/pom.xml
#	main/frontend-api/pom.xml
#	main/frontend-webdav/pom.xml
#	main/jacoco-report/pom.xml
#	main/keychain/pom.xml
#	main/launcher/pom.xml
#	main/pom.xml
#	main/uber-jar/pom.xml
#	main/ui/pom.xml
This commit is contained in:
Sebastian Stenzel
2017-04-20 16:11:37 +02:00
377 changed files with 2964 additions and 27586 deletions

View File

@@ -3,8 +3,8 @@
## Did you find a bug?
- Ensure you're running the latest version of Cryptomator.
- Ensure the bug is related to the desktop version of Cryptomator. Bugs concerning the Cryptomator iOS app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues).
- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [FAQ](https://cryptomator.org/faq/) and our [Wiki](https://github.com/cryptomator/cryptomator/wiki).
- Ensure the bug is related to the desktop version of Cryptomator. Bugs concerning the Cryptomator iOS and Android app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues) and [Cryptomator for Android issues list](https://github.com/cryptomator/cryptomator-android/issues) respectively.
- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [knowledge base](https://cryptomator.freshdesk.com/support/solutions) and our [Wiki](https://github.com/cryptomator/cryptomator/wiki).
- If you're unable to find an open issue addressing the problem, [submit a new one](https://github.com/cryptomator/cryptomator/issues/new).
## Do you have questions?
@@ -13,6 +13,11 @@
- [Contact us](https://cryptomator.org/contact/) directly by writing an email. Wir sprechen auch Deutsch!
- Have a chat with us on [Gitter](https://gitter.im/cryptomator/cryptomator).
## Do you miss a feature?
- Ensure the feature was not [already requested](https://github.com/cryptomator/cryptomator/issues).
- You're welcome to suggest a feature by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new).
## Did you write a patch that fixes a bug?
- Open a new pull request with the patch.

View File

@@ -1,19 +1,38 @@
### Basic Info
To tick a checkbox replace [ ] with [x]. Make sure to replace placeholders (…) accordingly.
- I'm running Cryptomator on: [Windows, OS X, and/or Debian (or other Linux Distribution), don't forget the version]
- I'm using Cryptomator in version: [you can check the version in the settings of Cryptomator]
## Issue Checklist
### Description
Before creating a new issue make sure that you
- [ ] searched [existing (and closed) issues](https://github.com/cryptomator/cryptomator/issues).
- [ ] searched the [knowledge base](https://cryptomator.freshdesk.com/support/solutions).
- [ ] have read the [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md).
- [ ] have read the [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md).
[description of the bug, question or feature - what did you do? what problem occurred? etc.]
## Basic Info
### Log File (optional)
This is a
- [ ] bug report.
- [ ] feature request.
- [ ] question or something else.
```
[insert relevant parts of the log file here if applicable,
don't forget to redact sensitive information
I'm using
- [ ] Windows in version: …
- [ ] macOS in version: …
- [ ] Linux in version: …
on Windows: %appdata%/Cryptomator/cryptomator.log
on OS X: ~/Library/Logs/Cryptomator/cryptomator.log
on Debian: ~/.Cryptomator/cryptomator.log]
```
I'm running Cryptomator in version: …
(You can check the version in the Cryptomator settings.)
## Description
(Please describe in detail what you did, what you expected, and what really happened.)
## Attachments (optional)
If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the [debug mode](https://cryptomator.freshdesk.com/support/solutions/articles/16000046480) first and reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file.
You can find the log file
- on Windows: %appdata%/Cryptomator/cryptomator.log
- on macOS: ~/Library/Logs/Cryptomator/cryptomator.log
- on Linux: ~/.Cryptomator/cryptomator.log

View File

@@ -45,7 +45,8 @@ For more information on the security details visit [cryptomator.org](https://cry
### Dependencies
* Java 8 + JCE unlimited strength policy files (needed for 256-bit keys)
* Java 8 (min. 8u51, we recommend to use the current version)
* [JCE unlimited strength policy files](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) (needed for 256-bit keys)
* Maven 3
* Optional: OS-dependent build tools for native packaging (see [Windows](https://github.com/cryptomator/cryptomator-win), [OS X](https://github.com/cryptomator/cryptomator-osx), [Linux](https://github.com/cryptomator/builder-containers))

View File

@@ -8,7 +8,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.4</version>
<version>1.3.0-rc1</version>
</parent>
<artifactId>ant-kit</artifactId>
<packaging>pom</packaging>
@@ -18,7 +18,7 @@
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
<artifactId>launcher</artifactId>
</dependency>
</dependencies>
@@ -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>

View File

@@ -3,15 +3,15 @@
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="\${java.class.path}:\${java.home}/../lib/ant-javafx.jar:." />
<!-- Define application to build -->
<fx:application id="Cryptomator" name="Cryptomator" version="${project.version}" mainClass="org.cryptomator.ui.Cryptomator" />
<fx:application id="Cryptomator" name="Cryptomator" version="${project.version}" mainClass="org.cryptomator.launcher.Cryptomator" />
<!-- Create main application jar -->
<target name="create-jar">
<fx:jar destfile="antbuild/Cryptomator-${project.version}.jar">
<fx:application refid="Cryptomator" />
<fx:fileset dir="libs" includes="ui-${project.version}.jar" />
<fx:fileset dir="libs" includes="launcher-${project.version}.jar" />
<fx:resources>
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="launcher-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
@@ -32,11 +32,12 @@
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:property name="cryptomator.ipcPortPath" value="~/.Cryptomator/ipcPort.bin" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="launcher-${project.version}.jar"/>
<fx:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
</fx:resources>
<fx:permissions elevated="false" />
@@ -55,11 +56,12 @@
<fx:property name="cryptomator.logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:property name="cryptomator.upgradeLogPath" value="~/.Cryptomator/upgrade.log" />
<fx:property name="cryptomator.settingsPath" value="~/.Cryptomator/settings.json" />
<fx:property name="cryptomator.ipcPortPath" value="~/.Cryptomator/ipcPort.bin" />
<fx:jvmarg value="-Xmx512m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="antbuild" type="jar" includes="Cryptomator-${project.version}.jar" />
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="ui-${project.version}.jar"/>
<fx:fileset dir="libs" type="jar" includes="*.jar" excludes="launcher-${project.version}.jar"/>
<fx:fileset dir="fixed-binaries" type="data" includes="linux-launcher-*" arch=""/>
</fx:resources>
<fx:permissions elevated="false" />

View File

@@ -1 +0,0 @@
/target/

View File

@@ -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;
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -10,10 +10,10 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>1.2.4</version>
<version>1.3.0-rc1</version>
</parent>
<artifactId>commons</artifactId>
<name>Cryptomator common</name>
<name>Cryptomator Commons</name>
<description>Shared utilities</description>
<dependencies>
@@ -26,6 +26,14 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.fxmisc.easybind</groupId>
<artifactId>easybind</artifactId>
</dependency>
<!-- DI -->
<dependency>
@@ -38,25 +46,10 @@
<scope>provided</scope>
</dependency>
<!-- Test -->
<!-- Logging -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -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);
}
}
}

View File

@@ -12,12 +12,51 @@ import java.util.Comparator;
import org.apache.commons.lang3.StringUtils;
/**
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
*/
public class SemVerComparator implements Comparator<String> {
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
@Override
public int compare(String version1, String version2) {
final String[] vComps1 = StringUtils.split(version1, '.');
final String[] vComps2 = StringUtils.split(version2, '.');
// "Build metadata SHOULD be ignored when determining version precedence.
// Thus two versions that differ only in the build metadata, have the same precedence."
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
return 0;
}
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
}
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
if (comparisonResult == 0) {
if (v1PreReleaseVersion.isEmpty()) {
return 1; // 1.0.0 > 1.0.0-BETA
} else if (v2PreReleaseVersion.isEmpty()) {
return -1; // 1.0.0-BETA < 1.0.0
} else {
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
}
} else {
return comparisonResult;
}
}
private int compareNumericallyThenLexicographically(String version1, String version2) {
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
for (int i = 0; i < commonCompCount; i++) {
@@ -35,7 +74,7 @@ public class SemVerComparator implements Comparator<String> {
}
}
// all in common so far? longest version string wins:
// all in common so far? longest version string is considered the higher version:
return vComps1.length - vComps2.length;
}

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* Copyright (c) 2014, 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
******************************************************************************/
package org.cryptomator.common.settings;
import java.util.function.Consumer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
public class Settings {
public static final int MIN_PORT = 1024;
public static final int MAX_PORT = 65535;
public static final boolean DEFAULT_CHECK_FOR_UDPATES = true;
public static final int DEFAULT_PORT = 42427;
public static final boolean DEFAULT_USE_IPV6 = false;
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final String DEFAULT_GVFS_SCHEME = "dav";
public static final boolean DEFAULT_DEBUG_MODE = false;
private final Consumer<Settings> saveCmd;
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList();
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
private final BooleanProperty useIpv6 = new SimpleBooleanProperty(DEFAULT_USE_IPV6);
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
private final StringProperty preferredGvfsScheme = new SimpleStringProperty(DEFAULT_GVFS_SCHEME);
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
directories.addListener((ListChangeListener.Change<? extends VaultSettings> change) -> this.save());
checkForUpdates.addListener(this::somethingChanged);
port.addListener(this::somethingChanged);
useIpv6.addListener(this::somethingChanged);
numTrayNotifications.addListener(this::somethingChanged);
preferredGvfsScheme.addListener(this::somethingChanged);
debugMode.addListener(this::somethingChanged);
}
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
this.save();
}
void save() {
if (saveCmd != null) {
saveCmd.accept(this);
}
}
/* Getter/Setter */
public ObservableList<VaultSettings> getDirectories() {
return directories;
}
public BooleanProperty checkForUpdates() {
return checkForUpdates;
}
public IntegerProperty port() {
return port;
}
public BooleanProperty useIpv6() {
return useIpv6;
}
public IntegerProperty numTrayNotifications() {
return numTrayNotifications;
}
public StringProperty preferredGvfsScheme() {
return preferredGvfsScheme;
}
public BooleanProperty debugMode() {
return debugMode;
}
}

View File

@@ -0,0 +1,103 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.settings;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
private final Consumer<Settings> saveCmd;
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
public SettingsJsonAdapter(Consumer<Settings> saveCmd) {
this.saveCmd = saveCmd;
}
@Override
public void write(JsonWriter out, Settings value) throws IOException {
out.beginObject();
out.name("directories");
writeVaultSettingsArray(out, value.getDirectories());
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
out.name("port").value(value.port().get());
out.name("useIpv6").value(value.useIpv6().get());
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get());
out.name("debugMode").value(value.debugMode().get());
out.endObject();
}
private void writeVaultSettingsArray(JsonWriter out, Iterable<VaultSettings> vaultSettings) throws IOException {
out.beginArray();
for (VaultSettings value : vaultSettings) {
vaultSettingsJsonAdapter.write(out, value);
}
out.endArray();
}
@Override
public Settings read(JsonReader in) throws IOException {
Settings settings = new Settings(saveCmd);
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "directories":
settings.getDirectories().addAll(readVaultSettingsArray(in, settings));
break;
case "checkForUpdatesEnabled":
settings.checkForUpdates().set(in.nextBoolean());
break;
case "port":
settings.port().set(in.nextInt());
break;
case "useIpv6":
settings.useIpv6().set(in.nextBoolean());
break;
case "numTrayNotifications":
settings.numTrayNotifications().set(in.nextInt());
break;
case "preferredGvfsScheme":
settings.preferredGvfsScheme().set(in.nextString());
break;
case "debugMode":
settings.debugMode().set(in.nextBoolean());
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
}
}
in.endObject();
return settings;
}
private List<VaultSettings> readVaultSettingsArray(JsonReader in, Settings settings) throws IOException {
List<VaultSettings> result = new ArrayList<>();
in.beginArray();
while (!JsonToken.END_ARRAY.equals(in.peek())) {
result.add(vaultSettingsJsonAdapter.read(in, settings));
}
in.endArray();
return result;
}
}

View File

@@ -6,17 +6,21 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.settings;
package org.cryptomator.common.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -25,16 +29,17 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@Singleton
public class SettingsProvider implements Provider<Settings> {
@@ -54,16 +59,21 @@ public class SettingsProvider implements Provider<Settings> {
}
}
private final ObjectMapper objectMapper;
private final ScheduledExecutorService saveScheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
private final AtomicReference<Settings> settings = new AtomicReference<>();
private final SettingsJsonAdapter settingsJsonAdapter = new SettingsJsonAdapter(this::scheduleSave);
private final Gson gson;
@Inject
public SettingsProvider(@Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
public SettingsProvider() {
this.gson = new GsonBuilder() //
.setPrettyPrinting().setLenient().disableHtmlEscaping() //
.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
.create();
}
private Path getSettingsPath() throws IOException {
private Path getSettingsPath() {
final String settingsPathProperty = System.getProperty("cryptomator.settingsPath");
return Optional.ofNullable(settingsPathProperty).filter(StringUtils::isNotBlank).map(this::replaceHomeDir).map(FileSystems.getDefault()::getPath).orElse(DEFAULT_SETTINGS_PATH);
}
@@ -78,14 +88,19 @@ public class SettingsProvider implements Provider<Settings> {
@Override
public Settings get() {
final Settings settings = new Settings(this::scheduleSave);
try {
final Path settingsPath = getSettingsPath();
final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ);
objectMapper.readerForUpdating(settings).readValue(in);
return LazyInitializer.initializeLazily(settings, this::load);
}
private Settings load() {
Settings settings;
final Path settingsPath = getSettingsPath();
try (InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ); //
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
settings = gson.fromJson(reader, Settings.class);
LOG.info("Settings loaded from " + settingsPath);
} catch (IOException e) {
LOG.info("Failed to load settings, creating new one.");
settings = new Settings(this::scheduleSave);
}
return settings;
}
@@ -104,13 +119,15 @@ public class SettingsProvider implements Provider<Settings> {
}
private void save(Settings settings) {
Objects.requireNonNull(settings);
assert settings != null : "method should only be invoked by #scheduleSave, which checks for null";
final Path settingsPath = getSettingsPath();
try {
final Path settingsPath = getSettingsPath();
Files.createDirectories(settingsPath.getParent());
final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
LOG.info("Settings saved to " + settingsPath);
try (OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
gson.toJson(settings, writer);
LOG.info("Settings saved to " + settingsPath);
}
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}

View File

@@ -0,0 +1,140 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.settings;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Objects;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.fxmisc.easybind.EasyBind;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
public class VaultSettings {
private final Settings settings;
private final String id;
private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
private final StringProperty mountName = new SimpleStringProperty();
private final StringProperty winDriveLetter = new SimpleStringProperty();
private final BooleanProperty mountAfterUnlock = new SimpleBooleanProperty();
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty();
public VaultSettings(Settings settings, String id) {
this.settings = settings;
this.id = Objects.requireNonNull(id);
EasyBind.subscribe(path, this::deriveMountNameFromPath);
path.addListener(this::somethingChanged);
mountName.addListener(this::somethingChanged);
winDriveLetter.addListener(this::somethingChanged);
mountAfterUnlock.addListener(this::somethingChanged);
}
private void somethingChanged(ObservableValue<?> observable, Object oldValue, Object newValue) {
settings.save();
}
private void deriveMountNameFromPath(Path path) {
if (path != null && StringUtils.isBlank(mountName.get())) {
mountName.set(normalizeMountName(path.getFileName().toString()));
}
}
public static VaultSettings withRandomId(Settings settings) {
return new VaultSettings(settings, generateId());
}
private static String generateId() {
return asBase64String(nineBytesFrom(UUID.randomUUID()));
}
private static String asBase64String(byte[] bytes) {
byte[] base64Bytes = Base64.getUrlEncoder().encode(bytes);
return new String(base64Bytes, StandardCharsets.US_ASCII);
}
private static byte[] nineBytesFrom(UUID uuid) {
ByteBuffer uuidBuffer = ByteBuffer.allocate(9);
uuidBuffer.putLong(uuid.getMostSignificantBits());
uuidBuffer.put((byte) (uuid.getLeastSignificantBits() & 0xFF));
uuidBuffer.flip();
return uuidBuffer.array();
}
public static String normalizeMountName(String mountName) {
String normalizedMountName = StringUtils.stripAccents(mountName);
StringBuilder builder = new StringBuilder();
for (char c : normalizedMountName.toCharArray()) {
if (Character.isWhitespace(c)) {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
} else if (c < 127 && Character.isLetterOrDigit(c)) {
builder.append(c);
} else {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
}
}
return builder.toString();
}
/* Getter/Setter */
public String getId() {
return id;
}
public ObjectProperty<Path> path() {
return path;
}
public StringProperty mountName() {
return mountName;
}
public StringProperty winDriveLetter() {
return winDriveLetter;
}
public BooleanProperty mountAfterUnlock() {
return mountAfterUnlock;
}
public BooleanProperty revealAfterMount() {
return revealAfterMount;
}
/* Hashcode/Equals */
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof VaultSettings && obj.getClass().equals(this.getClass())) {
VaultSettings other = (VaultSettings) obj;
return Objects.equals(this.id, other.id);
} else {
return false;
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.settings;
import java.io.IOException;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
class VaultSettingsJsonAdapter {
private static final Logger LOG = LoggerFactory.getLogger(VaultSettingsJsonAdapter.class);
public void write(JsonWriter out, VaultSettings value) throws IOException {
out.beginObject();
out.name("id").value(value.getId());
out.name("path").value(value.path().get().toString());
out.name("mountName").value(value.mountName().get());
out.name("winDriveLetter").value(value.winDriveLetter().get());
out.name("mountAfterUnlock").value(value.mountAfterUnlock().get());
out.name("revealAfterMount").value(value.revealAfterMount().get());
out.endObject();
}
public VaultSettings read(JsonReader in, Settings settings) throws IOException {
String id = null;
String path = null;
String mountName = null;
String winDriveLetter = null;
boolean mountAfterUnlock = true;
boolean revealAfterMount = true;
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
switch (name) {
case "id":
id = in.nextString();
break;
case "path":
path = in.nextString();
break;
case "mountName":
mountName = in.nextString();
break;
case "winDriveLetter":
winDriveLetter = in.nextString();
break;
case "mountAfterUnlock":
mountAfterUnlock = in.nextBoolean();
break;
case "revealAfterMount":
revealAfterMount = in.nextBoolean();
break;
default:
LOG.warn("Unsupported vault setting found in JSON: " + name);
in.skipValue();
}
}
in.endObject();
VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId(settings) : new VaultSettings(settings, id);
vaultSettings.mountName().set(mountName);
vaultSettings.path().set(Paths.get(path));
vaultSettings.winDriveLetter().set(winDriveLetter);
vaultSettings.mountAfterUnlock().set(mountAfterUnlock);
vaultSettings.revealAfterMount().set(revealAfterMount);
return vaultSettings;
}
}

View File

@@ -10,7 +10,6 @@ package org.cryptomator.common;
import java.util.Comparator;
import org.cryptomator.common.SemVerComparator;
import org.junit.Assert;
import org.junit.Test;
@@ -22,8 +21,10 @@ public class SemVerComparatorTest {
@Test
public void compareEqualVersions() {
final int comparisonResult = semVerComparator.compare("1.23.4", "1.23.4");
Assert.assertEquals(0, Integer.signum(comparisonResult));
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
Assert.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
}
// newer versions in first argument
@@ -33,7 +34,11 @@ public class SemVerComparatorTest {
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4a", "1.23.4")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
Assert.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
}
// newer versions in second argument
@@ -43,7 +48,11 @@ public class SemVerComparatorTest {
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4a")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
Assert.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
}
}

View File

@@ -0,0 +1,40 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.settings;
import java.io.IOException;
import org.junit.Assert;
import org.junit.Test;
public class SettingsJsonAdapterTest {
private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(this::noop);
private void noop(Settings settings) {
}
@Test
public void testDeserialize() throws IOException {
String vault1Json = "{\"id\": \"1\", \"path\": \"/vault1\", \"mountName\": \"vault1\", \"winDriveLetter\": \"X\"}";
String vault2Json = "{\"id\": \"2\", \"path\": \"/vault2\", \"mountName\": \"vault2\", \"winDriveLetter\": \"Y\"}";
String json = "{\"directories\": [" + vault1Json + "," + vault2Json + "]," //
+ "\"checkForUpdatesEnabled\": true,"//
+ "\"port\": 8080,"//
+ "\"useIpv6\": true,"//
+ "\"numTrayNotifications\": 42}";
Settings settings = adapter.fromJson(json);
Assert.assertTrue(settings.checkForUpdates().get());
Assert.assertEquals(2, settings.getDirectories().size());
Assert.assertEquals(8080, settings.port().get());
Assert.assertTrue(settings.useIpv6().get());
Assert.assertEquals(42, settings.numTrayNotifications().get());
Assert.assertEquals("dav", settings.preferredGvfsScheme().get());
}
}

View File

@@ -0,0 +1,35 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.settings;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Paths;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import com.google.gson.stream.JsonReader;
public class VaultSettingsJsonAdapterTest {
private final VaultSettingsJsonAdapter adapter = new VaultSettingsJsonAdapter();
@Test
public void testDeserialize() throws IOException {
String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"mountName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true}";
JsonReader jsonReader = new JsonReader(new StringReader(json));
Settings settings = Mockito.mock(Settings.class);
VaultSettings vaultSettings = adapter.read(jsonReader, settings);
Assert.assertEquals("foo", vaultSettings.getId());
Assert.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
Assert.assertEquals("test", vaultSettings.mountName().get());
Assert.assertEquals("X", vaultSettings.winDriveLetter().get());
}
}

View File

@@ -6,21 +6,21 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.ui.model;
package org.cryptomator.common.settings;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VaultTest {
public class VaultSettingsTest {
@Test
public void testNormalize() throws Exception {
assertEquals("_", Vault.normalize(" "));
assertEquals("a", Vault.normalize("ä"));
assertEquals("C", Vault.normalize("Ĉ"));
assertEquals("_", Vault.normalize(":"));
assertEquals("", Vault.normalize("汉语"));
assertEquals("_", VaultSettings.normalizeMountName(" "));
assertEquals("a", VaultSettings.normalizeMountName("ä"));
assertEquals("C", VaultSettings.normalizeMountName("Ĉ"));
assertEquals("_", VaultSettings.normalizeMountName(":"));
assertEquals("_", VaultSettings.normalizeMountName("汉语"));
}
}

View File

@@ -1,2 +0,0 @@
/target/
/target/

View File

@@ -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 + ")");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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());
}
}
}

View File

@@ -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 -> {
};
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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"));
}
}

View File

@@ -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());
}
}

View File

@@ -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));
}
}

View File

@@ -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"));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -1 +0,0 @@
/target/

View File

@@ -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);
}
};
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}));
}
}

View File

@@ -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>

View File

@@ -1 +0,0 @@
/target/

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 * costParam / 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
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More