Merge branch 'develop' into feature/javafx-color-scheme-change

This commit is contained in:
Armin Schrenk
2026-02-12 10:18:32 +01:00
committed by GitHub
15 changed files with 344 additions and 4 deletions
@@ -0,0 +1,97 @@
package org.cryptomator.launcher;
import org.slf4j.Logger;
import java.io.IOException;
import java.io.Reader;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Properties;
import java.util.Set;
/**
* Factory to generate admin properties.
*
* <p>
* Admin properties are {@link Properties} using system properties as defaults, but allow overwriting a specific set of properties with an external config file.
* Those properties are created by calling {@link #create()}. The method first reads system property {@value #ADMIN_PROP_FILE_KEY}. If it contains a path to a valid properties file, all overridable properties from the file are loaded into the returned admin properties.
* <p>
* The overridable properties are:
* <ul>
* <li>cryptomator.logDir</li>
* <li>cryptomator.pluginDir</li>
* <li>cryptomator.p12Path</li>
* <li>cryptomator.mountPointsDir</li>
* <li>cryptomator.disableUpdateCheck</li>
* </ul>
*
* @see Properties
* @see System#getProperties()
*/
class AdminPropertiesFactory {
private static final Logger LOG = EventualLogger.INSTANCE;
private static final long MAX_CONFIG_SIZE_BYTES = 8192;
private static final String ADMIN_PROP_FILE_KEY = "cryptomator.adminConfigPath";
private static final Set<String> ALLOWED_OVERRIDES = Set.of( //
"cryptomator.logDir", //
"cryptomator.pluginDir", //
"cryptomator.p12Path", //
"cryptomator.mountPointsDir", //
"cryptomator.disableUpdateCheck");
/**
* Creates new {@link Properties} containing overridable properties from the admin config.
* <p>
* The returned properties object uses as default the {@link System} properties.
* For a list of overridable properties, see {@link AdminPropertiesFactory}
*
* @return {@link Properties} containing overridable properties from the admin config and defaulting to system properties.
*/
static Properties create() {
var systemProps = System.getProperties();
var adminProps = new Properties(systemProps);
final String adminCfgPath = System.getProperty(ADMIN_PROP_FILE_KEY);
if (adminCfgPath == null) {
LOG.debug("Admin config property is not defined. Skipping.");
return adminProps;
}
var propsFromFile = loadPropertiesFromFile(Path.of(adminCfgPath));
for (var key : propsFromFile.stringPropertyNames()) {
if (ALLOWED_OVERRIDES.contains(key)) {
var value = propsFromFile.getProperty(key);
LOG.info("Overwriting {} with value {} from admin config.", key, value);
adminProps.setProperty(key, value);
} else {
LOG.debug("Property {} in admin config is not supported for override.", key);
}
}
return adminProps;
}
//visible for testing
static Properties loadPropertiesFromFile(Path adminPropertiesPath) {
var adminProps = new Properties();
try (FileChannel ch = FileChannel.open(adminPropertiesPath, StandardOpenOption.READ); //
Reader reader = Channels.newReader(ch, StandardCharsets.UTF_8)) {
if (ch.size() > MAX_CONFIG_SIZE_BYTES) {
throw new IOException("Config file %s exceeds maximum size of %d".formatted(adminPropertiesPath, MAX_CONFIG_SIZE_BYTES));
}
adminProps.load(reader);
} catch (NoSuchFileException _) {
//NO-OP
LOG.debug("No admin properties found at {}.", adminPropertiesPath);
} catch (IOException | IllegalArgumentException e) {
LOG.warn("Failed to read administrative properties from {}. Returning empty properties.", adminPropertiesPath, e);
}
return adminProps;
}
}
@@ -11,9 +11,9 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.SubstitutingProperties;
import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ipc.IpcCommunicator;
import org.cryptomator.logging.DebugMode;
import org.cryptomator.networking.SSLContextProvider;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +35,8 @@ public class Cryptomator {
private static final long STARTUP_TIME = System.currentTimeMillis();
static {
var lazyProcessedProps = new SubstitutingProperties(System.getProperties(), System.getenv());
var adminProps = AdminPropertiesFactory.create();
var lazyProcessedProps = new SubstitutingProperties(adminProps, System.getenv());
System.setProperties(lazyProcessedProps);
CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
LOG = LoggerFactory.getLogger(Cryptomator.class);
@@ -64,6 +65,7 @@ public class Cryptomator {
}
public static void main(String[] args) {
EventualLogger.INSTANCE.drainTo(LOG);
var printVersion = Optional.ofNullable(args) //
.stream() //Streams either one element (the args-array) or zero elements
.flatMap(Arrays::stream) //
@@ -0,0 +1,106 @@
package org.cryptomator.launcher;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.event.DefaultLoggingEvent;
import org.slf4j.event.Level;
import org.slf4j.event.LoggingEvent;
import org.slf4j.helpers.AbstractLogger;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
class EventualLogger extends AbstractLogger {
static final EventualLogger INSTANCE = new EventualLogger();
private final Queue<LoggingEvent> bufferedEvents = new ArrayDeque<>();
private EventualLogger() {
}
synchronized void drainTo(Logger gutter) {
for (var event : bufferedEvents) {
var builder = gutter.atLevel(event.getLevel()) //
.setCause(event.getThrowable()) //
.setMessage(event.getMessage());
event.getArguments().forEach(builder::addArgument);
Objects.requireNonNullElse(event.getMarkers(), List.<Marker>of()).forEach(builder::addMarker);
builder.log();
}
bufferedEvents.clear();
}
@Override
protected synchronized void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) {
var event = new DefaultLoggingEvent(level, this);
if (marker != null) {
event.addMarker(marker);
}
event.setMessage(messagePattern);
for (var arg : Objects.requireNonNullElse(arguments, new Object[]{})) {
event.addArgument(arg);
}
event.setThrowable(throwable);
bufferedEvents.add(event);
}
//Unclear, unused and undocumented method of slf4j, see also https://github.com/qos-ch/slf4j/discussions/348
@Override
protected String getFullyQualifiedCallerName() {
return getClass().getCanonicalName();
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public boolean isTraceEnabled(Marker marker) {
return true;
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isDebugEnabled(Marker marker) {
return true;
}
@Override
public boolean isInfoEnabled() {
return true;
}
@Override
public boolean isInfoEnabled(Marker marker) {
return true;
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public boolean isWarnEnabled(Marker marker) {
return true;
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public boolean isErrorEnabled(Marker marker) {
return true;
}
}
@@ -0,0 +1,97 @@
package org.cryptomator.launcher;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Properties;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.hasEntry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
public class AdminPropertiesFactoryTest {
private static final String PROPS = """
fruit=banana
vegetable:kärrot
method=scan寧""";
@Test
@DisplayName("UTF-8 is supported")
void loadUTF8Properties(@TempDir Path path) throws IOException {
var config = path.resolve("config.properties");
try (var out = Files.newOutputStream(config, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
var bytes = PROPS.getBytes(StandardCharsets.UTF_8);
out.write(bytes);
}
var properties = AdminPropertiesFactory.loadPropertiesFromFile(config);
Assertions.assertAll(List.of( //
() -> MatcherAssert.assertThat(properties, hasEntry("fruit", "banana")), //
() -> MatcherAssert.assertThat(properties, hasEntry("vegetable", "kärrot")), //
() -> MatcherAssert.assertThat(properties, hasEntry("method", "scan寧"))));
}
@Test
@DisplayName("Loading not existing file returns empty properties")
void loadNotExistingFile(@TempDir Path path) {
var config = path.resolve("config.properties");
var properties = AdminPropertiesFactory.loadPropertiesFromFile(config);
MatcherAssert.assertThat(properties, anEmptyMap());
}
@Test
@DisplayName("Loading invalid file returns empty properties")
void loadInvalidFile(@TempDir Path path) throws IOException {
var config = path.resolve("config.properties");
try (var out = Files.newOutputStream(config, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
var bytes = "method=\\u2u20".getBytes(StandardCharsets.UTF_8); //only one "u" is allowed in a Unicode escape sequence
out.write(bytes);
}
var properties = AdminPropertiesFactory.loadPropertiesFromFile(config);
MatcherAssert.assertThat(properties, anEmptyMap());
}
@Test
@DisplayName("Loading too big file returns empty properties")
void loadTooBigFile(@TempDir Path path) throws IOException {
var config = path.resolve("config.properties");
try (var channel = Files.newByteChannel(config, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
channel.position(10_000);
channel.write(ByteBuffer.wrap("test=test".getBytes()));
}
var properties = AdminPropertiesFactory.loadPropertiesFromFile(config);
MatcherAssert.assertThat(properties, anEmptyMap());
}
@Test
@DisplayName("If system properties do not contain config path, skip loading")
void skipLoadIfFilePathIsNotDefined() {
Assertions.assertNull(System.getProperty("cryptomator.adminConfigPath"));
try (var adminPropSetterMock = mockStatic(AdminPropertiesFactory.class)) {
adminPropSetterMock.when(AdminPropertiesFactory::create).thenCallRealMethod();
adminPropSetterMock.when(() -> AdminPropertiesFactory.loadPropertiesFromFile(any())).thenReturn(new Properties());
var adminProps = AdminPropertiesFactory.create();
adminPropSetterMock.verify(() -> AdminPropertiesFactory.loadPropertiesFromFile(any()), never());
Assertions.assertEquals(System.getProperty("user.home"), adminProps.getProperty("user.home"));
}
}
}