diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 995b155..c728d22 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -18,6 +18,7 @@
requires org.purejava.appindicator;
requires org.purejava.kwallet;
requires de.swiesend.secretservice;
+ requires java.xml;
provides AutoStartProvider with FreedesktopAutoStartService;
provides KeychainAccessProvider with GnomeKeyringKeychainAccess, KDEWalletKeychainAccess;
diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java
index 6570d88..22cf207 100644
--- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java
+++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java
@@ -6,18 +6,40 @@
import org.cryptomator.integrations.common.Priority;
import org.cryptomator.integrations.quickaccess.QuickAccessService;
import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.List;
import java.util.UUID;
/**
@@ -29,20 +51,11 @@
@Priority(90)
public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
+ private static final Logger LOG = LoggerFactory.getLogger(DolphinPlaces.class);
+
+ private static final String XBEL_NAMESPACE = "http://www.freedesktop.org/standards/desktop-bookmarks";
private static final int MAX_FILE_SIZE = 1 << 20; //1MiB, xml is quite verbose
private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel");
- private static final String ENTRY_TEMPLATE = """
-
{@code
+ *
+ * integrations-linux
+ *
+ *
+ *
+ *
+ *
+ * sldkf-sadf-sadf-sadf
+ *
+ *
+ *
+ * }
+ *
+ * @param target The mount point of the vault
+ * @param displayName Caption of the vault link in dolphin
+ * @param xmlDocument The xbel document to which the bookmark should be added
+ *
+ * @throws QuickAccessServiceException if the bookmark could not be created
+ */
+ private void createBookmark(Path target, String displayName, String id, Document xmlDocument) throws QuickAccessServiceException {
+ try {
+ var bookmark = xmlDocument.createElement("bookmark");
+ var title = xmlDocument.createElement("title");
+ var info = xmlDocument.createElement("info");
+ var metadataBookmark = xmlDocument.createElement("metadata");
+ var metadataOwner = xmlDocument.createElement("metadata");
+ var bookmarkIcon = xmlDocument.createElementNS(XBEL_NAMESPACE, "bookmark:icon");
+ var idElem = xmlDocument.createElement("id");
+ bookmark.setAttribute("href", target.toUri().toString());
+ title.setTextContent(displayName);
+ bookmark.appendChild(title);
+ bookmark.appendChild(info);
+ info.appendChild(metadataBookmark);
+ info.appendChild(metadataOwner);
+ metadataBookmark.appendChild(bookmarkIcon);
+ metadataOwner.appendChild(idElem);
+ metadataBookmark.setAttribute("owner", "http://freedesktop.org");
+ bookmarkIcon.setAttribute("name","drive-harddisk-encrypted");
+ metadataOwner.setAttribute("owner", "https://cryptomator.org");
+ idElem.setTextContent(id);
+ xmlDocument.getDocumentElement().appendChild(bookmark);
+ } catch (DOMException | IllegalArgumentException e) {
+ throw new QuickAccessServiceException("Error while creating bookmark for target: " + target, e);
+ }
}
private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
@@ -97,46 +238,20 @@ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implemen
@Override
public String removeEntryFromConfig(String config) throws QuickAccessServiceException {
try {
- int idIndex = config.lastIndexOf(id);
- if (idIndex == -1) {
- return config; //assume someone has removed our entry, nothing to do
- }
- //validate
- XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
- //modify
- int openingTagIndex = indexOfEntryOpeningTag(config, idIndex);
- var contentToWrite1 = config.substring(0, openingTagIndex).stripTrailing();
-
- int closingTagEndIndex = config.indexOf('>', config.indexOf(" tag.");
- }
}
@CheckAvailability
public static boolean isSupported() {
return Files.exists(PLACES_FILE);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java
index 70ed6c1..44d74fd 100644
--- a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java
+++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java
@@ -31,10 +31,20 @@ abstract class FileConfiguredQuickAccess implements QuickAccessService {
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
}
+ /**
+ *
+ * Adds the vault path to the quick-access config file
+ *
+ * @param target The mount point of the vault
+ * @param displayName Caption of the vault link
+ * @return A cleanup reference for vault link removal
+ * @throws QuickAccessServiceException If the entry could not be added to the quick-access config file
+ */
@Override
public QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
try {
modifyLock.lock();
+ checkFileSize();
var entryAndConfig = addEntryToConfig(readConfig(), target, displayName);
persistConfig(entryAndConfig.config());
return entryAndConfig.entry();
diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java
index 0075374..7400c6f 100644
--- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java
+++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java
@@ -1,14 +1,155 @@
package org.cryptomator.linux.quickaccess;
-import org.junit.jupiter.api.Assertions;
+import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
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.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import static org.junit.jupiter.api.Assertions.*;
public class DolphinPlacesTest {
+ private static final String UUID_FOLDER_1 = "c4b72799-ca67-4c2e-b727-99ca67dc2e5d";
+ private static final String UUID_FOLDER_1_IDENTICAL = "43c6fdb9-626d-468e-86fd-b9626dc68e04d";
+ private static final String CAPTION_FOLDER_1 = "folder 1";
+ private static final String PATH_FOLDER_1 = "/home/someuser/folder1";
+
+ private static final String RESOURCE_USER_PLACES = "quickaccess/dolphin/user-places.xbel";
+ private static final String RESOURCE_USER_PLACES_MULTIPLE_IDENTICAL = "quickaccess/dolphin/user-places-multiple-identical.xbel";
+ private static final String RESOURCE_USER_PLACES_NOT_WELL_FORMED = "quickaccess/dolphin/user-places-not-well-formed.xbel";
+ private static final String RESOURCE_USER_PLACES_NOT_VALID = "quickaccess/dolphin/user-places-not-valid.xbel";
+
@Test
@DisplayName("Class can be loaded and object instantiated")
public void testInit() {
- Assertions.assertDoesNotThrow(DolphinPlaces::new);
+ assertDoesNotThrow(() -> { new DolphinPlaces(); });
+ }
+
+ @Test
+ @DisplayName("Adding an identical entry should lead to a replacement of the existing entry")
+ public void addingAnIdenticalEntryShouldLeadToReplacementOfExistingEntry(@TempDir Path tmpdir) {
+
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES, tmpdir);
+ assertTrue(loadFile(pathToDoc).contains(UUID_FOLDER_1));
+ assertTrue(loadFile(pathToDoc).contains(CAPTION_FOLDER_1));
+ assertDoesNotThrow(() -> {
+ var entry = new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1);
+ assertFalse(loadFile(pathToDoc).contains(UUID_FOLDER_1));
+ assertTrue(loadFile(pathToDoc).contains(CAPTION_FOLDER_1));
+ entry.remove();
+ });
+ }
+
+ @Test
+ @DisplayName("Adding an identical entry should lead to a replacement of multiple existing entries")
+ public void addingAnIdenticalEntryShouldLeadToReplacementOfMultipleExistingEntry(@TempDir Path tmpdir) {
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES_MULTIPLE_IDENTICAL, tmpdir);
+ assertEquals(1, countOccurrences(loadFile(pathToDoc),UUID_FOLDER_1));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc),UUID_FOLDER_1_IDENTICAL));
+ assertEquals(2, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ assertDoesNotThrow(() -> {
+ var entry = new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1);
+ assertEquals(0, countOccurrences(loadFile(pathToDoc),UUID_FOLDER_1));
+ assertEquals(0, countOccurrences(loadFile(pathToDoc),UUID_FOLDER_1_IDENTICAL));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ entry.remove();
+ });
+ assertEquals(0, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ }
+
+ @Test
+ @DisplayName("Adding should not replace if file is not valid")
+ public void addingShouldNotReplaceIfFileIsNotValid(@TempDir Path tmpdir) {
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES_NOT_VALID, tmpdir);
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ assertThrows(QuickAccessServiceException.class, () -> {
+ new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1);
+ });
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ }
+
+ @Test
+ @DisplayName("Adding should not replace if file is not well formed")
+ public void addingShouldNotReplaceIfFileIsNotWellFormed(@TempDir Path tmpdir) {
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES_NOT_WELL_FORMED, tmpdir);
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ assertThrows(QuickAccessServiceException.class, () -> {
+ new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1);
+ });
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1));
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1));
+ }
+
+ @Test
+ @DisplayName("Invalid characters in caption should be escaped")
+ public void invalidCharactersInCaptionShouldBeEscaped(@TempDir Path tmpdir) {
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES, tmpdir);
+ assertEquals(0, countOccurrences(loadFile(pathToDoc), "< & >"));
+ assertDoesNotThrow(() -> {
+ new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), "< & >");
+ });
+ assertEquals(1, countOccurrences(loadFile(pathToDoc), "< & >"));
+ }
+
+ @Test
+ @DisplayName("The xml file root object should not be changed when adding an entry")
+ public void xmlFileRootObjectShouldNotBeChangedWhenAddingAnEntry(@TempDir Path tmpdir) throws IOException {
+ var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES, tmpdir);
+ var rootObject = """
+
+
+
+