From 4c1e1355b6e9856d69344512a5eef63f3a01cfdc Mon Sep 17 00:00:00 2001 From: jrauber Date: Fri, 6 Jun 2025 00:38:43 +0200 Subject: [PATCH 01/12] Refactors Dolphin quick access integration for robustness - Fixes #3883 This commit refactors the Dolphin quick access integration to use DOM manipulation for adding and removing bookmarks in the XBEL file. This change aims to improve the reliability and correctness of bookmark management by handling stale entries. Changes - Replaces string-based manipulation of the XBEL file with DOM manipulation using org.w3c.dom. - Introduces methods for loading, validating, and writing the XBEL document. - Implements bookmark creation and removal using XPath queries and DOM operations. - Adds more test cases to DolphinPlacesTest to cover adding and removing bookmarks under different scenarios. Impact - Improves the robustness of the Dolphin quick access integration by using standard XML parsing and manipulation techniques. - The tests are modified to verify correct bookmark management. Fixes: https://github.com/cryptomator/cryptomator/issues/3883 Author: jrauber Date: Fri Jun 6 00:38:43 2025 +0200 --- .idea/developer-tools.xml | 6 + src/main/java/module-info.java | 1 + .../linux/quickaccess/DolphinPlaces.java | 315 +++++++++++++++--- .../FileConfiguredQuickAccess.java | 9 + .../linux/quickaccess/DolphinPlacesTest.java | 165 ++++++++- .../user-places-multiple-identical.xbel | 28 ++ .../dolphin/user-places-not-valid.xbel | 42 +++ .../dolphin/user-places-not-well-formed.xbel | 39 +++ .../quickaccess/dolphin/user-places.xbel | 39 +++ 9 files changed, 587 insertions(+), 57 deletions(-) create mode 100644 .idea/developer-tools.xml create mode 100644 src/test/resources/quickaccess/dolphin/user-places-multiple-identical.xbel create mode 100644 src/test/resources/quickaccess/dolphin/user-places-not-valid.xbel create mode 100644 src/test/resources/quickaccess/dolphin/user-places-not-well-formed.xbel create mode 100644 src/test/resources/quickaccess/dolphin/user-places.xbel diff --git a/.idea/developer-tools.xml b/.idea/developer-tools.xml new file mode 100644 index 0000000..01cefa5 --- /dev/null +++ b/.idea/developer-tools.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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..87773ca 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -6,18 +6,41 @@ 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.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.transform.OutputKeys; import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +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.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathVariableResolver; +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.ArrayList; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; /** @@ -29,28 +52,29 @@ @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 = """ - - %s - - - - - - %s - - - """; + private static final String HOME_DIR = System.getProperty("user.home"); + private static final String CONFIG_PATH_IN_HOME = ".local/share"; + private static final String CONFIG_FILE_NAME = "user-places.xbel"; + private static final Path PLACES_FILE = Path.of(HOME_DIR,CONFIG_PATH_IN_HOME, CONFIG_FILE_NAME); private static final Validator XML_VALIDATOR; static { - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + try (var schemaDefinition = DolphinPlaces.class.getResourceAsStream("/xbel-1.0.xsd")) { + + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + Source schemaFile = new StreamSource(schemaDefinition); XML_VALIDATOR = factory.newSchema(schemaFile).newValidator(); + } catch (IOException | SAXException e) { throw new IllegalStateException("Failed to load included XBEL schema definition file.", e); } @@ -61,29 +85,200 @@ public DolphinPlaces() { super(PLACES_FILE, MAX_FILE_SIZE); } + public DolphinPlaces(Path configFilePath) { + super(configFilePath.resolve(CONFIG_FILE_NAME), MAX_FILE_SIZE); + } + @Override EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { + try { + String id = UUID.randomUUID().toString(); - //validate + + LOG.trace("Adding bookmark for target: '{}', displayName: '{}', id: '{}'", target, displayName, id); + + // Validate the existing config before modifying it, if it is invalid + // we should not modify it. XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); - // modify - int insertIndex = config.lastIndexOf("= 0; i--) { + Node node = nodeList.item(i); + node.getParentNode().removeChild(node); + } + } + + private NodeList extractBookmarksByPath(Path target, Document xmlDocument) throws QuickAccessServiceException { + + try { + + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPath xpath = xpathFactory.newXPath(); + + SimpleVariableResolver variableResolver = new SimpleVariableResolver(); + + variableResolver.addVariable(new QName("uri"), target.toUri().toString()); + + xpath.setXPathVariableResolver(variableResolver); + + String expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][@href=$uri]"; + + return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + + } catch (Exception e) { + throw new QuickAccessServiceException("Failed to extract bookmarks by path", e); + } + } + + private NodeList extractBookmarksById(String id, Document xmlDocument) throws QuickAccessServiceException { + + try { + + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPath xpath = xpathFactory.newXPath(); + + SimpleVariableResolver variableResolver = new SimpleVariableResolver(); + + variableResolver.addVariable(new QName("id"), id); + + xpath.setXPathVariableResolver(variableResolver); + + String expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][info/metadata/id[text()=$id]]"; + + return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); + + } catch (Exception e) { + throw new QuickAccessServiceException("Failed to extract bookmarks by id", e); } } - private String escapeXML(String s) { - return s.replace("&","&") // - .replace("<","<") // - .replace(">",">"); + private Document loadXmlDocument(String config) throws QuickAccessServiceException { + + try { + + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + + builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + builderFactory.setXIncludeAware(false); + builderFactory.setExpandEntityReferences(false); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + builderFactory.setNamespaceAware(true); + + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + + // Prevent external entities from being resolved + builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); + + return builder.parse(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))); + + } catch (Exception e) { + throw new QuickAccessServiceException("Failed to parse the xbel bookmark file", e); + } + } + + private String documentToString(Document xmlDocument) throws QuickAccessServiceException { + + try { + + StringWriter buf = new StringWriter(); + + Transformer xform = TransformerFactory.newInstance().newTransformer(); + xform.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + xform.setOutputProperty(OutputKeys.INDENT, "yes"); + xform.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); + xform.transform(new DOMSource(xmlDocument), new StreamResult(buf)); + + return buf.toString(); + + } catch (Exception e) { + throw new QuickAccessServiceException("Failed to read document into string", e); + } + } + + /** + * + * Adds a xml bookmark element to the specified xml document + * + *
{@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 (Exception e) { + throw new QuickAccessServiceException("Failed to insert bookmark for target: " + target, e); + } } private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry { @@ -96,42 +291,52 @@ 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(" vars = new HashMap<>(); /** - * Returns the start index (inclusive) of the {@link DolphinPlaces#ENTRY_TEMPLATE} entry - * @param placesContent the content of the XBEL places file - * @param idIndex start index (inclusive) of the entrys id tag value - * @return start index of the first bookmark tag, searching backwards from idIndex + * Adds a variable to the resolver. + * + * @param name The name of the variable + * @param value The value of the variable */ - private int indexOfEntryOpeningTag(String placesContent, int idIndex) { - var xmlWhitespaceChars = List.of(' ', '\t', '\n'); - for (char c : xmlWhitespaceChars) { - int idx = placesContent.lastIndexOf(" tag."); + public void addVariable(QName name, Object value) { + vars.put(name, value); + } + + /** + * Resolves a variable by its name. + * + * @param variableName The name of the variable to resolve + * @return The value of the variable, or null if not found + */ + public Object resolveVariable(QName variableName) { + return vars.get(variableName); } } diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java index 70ed6c1..6f2dc26 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java @@ -31,6 +31,15 @@ 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 { diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java index 0075374..5eb2054 100644 --- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java +++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java @@ -1,14 +1,175 @@ 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).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).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).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).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).add(Path.of(PATH_FOLDER_1), "< & >"); + + }); + + assertEquals(1, countOccurrences(loadFile(pathToDoc), "< & >")); + } + + private Path loadResourceToDir(String source, Path targetDir) { + + try (var stream = this.getClass().getClassLoader().getResourceAsStream(source)) { + + if (stream == null) { + throw new IOException("Resource not found: " + source); + } + + Files.copy(stream, targetDir.resolve("user-places.xbel"), StandardCopyOption.REPLACE_EXISTING); + + return targetDir.resolve("user-places.xbel"); + + } catch (IOException e) { + throw new RuntimeException("Failed to load resource: " + source, e); + } + } + + private String loadFile(Path file) { + + if (!Files.exists(file)) { + throw new RuntimeException("File does not exist: " + file); + } + + try { + return Files.readString(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private int countOccurrences(String content, String searchString) { + int count = 0; + int index = 0; + + while ((index = content.indexOf(searchString, index)) != -1) { + count++; + index += searchString.length(); + } + + return count; } } diff --git a/src/test/resources/quickaccess/dolphin/user-places-multiple-identical.xbel b/src/test/resources/quickaccess/dolphin/user-places-multiple-identical.xbel new file mode 100644 index 0000000..c6dc588 --- /dev/null +++ b/src/test/resources/quickaccess/dolphin/user-places-multiple-identical.xbel @@ -0,0 +1,28 @@ + + + + + + folder 1 + + + + + + c4b72799-ca67-4c2e-b727-99ca67dc2e5d + + + + + folder 1 + + + + + + 43c6fdb9-626d-468e-86fd-b9626dc68e04d + + + + \ No newline at end of file diff --git a/src/test/resources/quickaccess/dolphin/user-places-not-valid.xbel b/src/test/resources/quickaccess/dolphin/user-places-not-valid.xbel new file mode 100644 index 0000000..0f2080b --- /dev/null +++ b/src/test/resources/quickaccess/dolphin/user-places-not-valid.xbel @@ -0,0 +1,42 @@ + + + + + + folder 1 + + + + + + c4b72799-ca67-4c2e-b727-99ca67dc2e5d + + + + + folder 2 + + + + + + 50e2de06-d9c7-4f45-a2de-06d9c75f4523 + + + + + folder 3 + + + + + + 8ce76c5d-62b3-44c1-a76c-5d62b3a4c1f2 + + + + + + + diff --git a/src/test/resources/quickaccess/dolphin/user-places-not-well-formed.xbel b/src/test/resources/quickaccess/dolphin/user-places-not-well-formed.xbel new file mode 100644 index 0000000..835fcfc --- /dev/null +++ b/src/test/resources/quickaccess/dolphin/user-places-not-well-formed.xbel @@ -0,0 +1,39 @@ + + + + + + folder 1 + + metadata owner="http://freedesktop.org"> + bookmark:icon name="drive-harddisk-encrypted"/> + /metadata> + + c4b72799-ca67-4c2e-b727-99ca67dc2e5d + + + + + folder 2 + + + + + + 50e2de06-d9c7-4f45-a2de-06d9c75f4523 + + + + + folder 3 + + + + + + 8ce76c5d-62b3-44c1-a76c-5d62b3a4c1f2 + + + + diff --git a/src/test/resources/quickaccess/dolphin/user-places.xbel b/src/test/resources/quickaccess/dolphin/user-places.xbel new file mode 100644 index 0000000..3549620 --- /dev/null +++ b/src/test/resources/quickaccess/dolphin/user-places.xbel @@ -0,0 +1,39 @@ + + + + + + folder 1 + + + + + + c4b72799-ca67-4c2e-b727-99ca67dc2e5d + + + + + folder 2 + + + + + + 50e2de06-d9c7-4f45-a2de-06d9c75f4523 + + + + + folder 3 + + + + + + 8ce76c5d-62b3-44c1-a76c-5d62b3a4c1f2 + + + + From ec6455bcb6b97179330a570377bccedf03d3d01f Mon Sep 17 00:00:00 2001 From: jrauber Date: Sun, 15 Jun 2025 23:17:11 +0200 Subject: [PATCH 02/12] Add a unit test to check that the same start sequence of the file is not changed. --- .../linux/quickaccess/DolphinPlaces.java | 45 +++++++++++-------- .../linux/quickaccess/DolphinPlacesTest.java | 28 ++++++++++++ 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 87773ca..3e56025 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -11,7 +11,6 @@ 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; @@ -38,7 +37,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -69,8 +67,8 @@ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAcc SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + //factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + //factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); Source schemaFile = new StreamSource(schemaDefinition); XML_VALIDATOR = factory.newSchema(schemaFile).newValidator(); @@ -179,19 +177,17 @@ private Document loadXmlDocument(String config) throws QuickAccessServiceExcepti DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); - builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - builderFactory.setXIncludeAware(false); - builderFactory.setExpandEntityReferences(false); - builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - builderFactory.setNamespaceAware(true); + //builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + //builderFactory.setXIncludeAware(false); + //builderFactory.setExpandEntityReferences(false); + //builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + //builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + //builderFactory.setNamespaceAware(true); DocumentBuilder builder = builderFactory.newDocumentBuilder(); // Prevent external entities from being resolved - builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); + //builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); return builder.parse(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))); @@ -206,13 +202,24 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE StringWriter buf = new StringWriter(); - Transformer xform = TransformerFactory.newInstance().newTransformer(); - xform.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - xform.setOutputProperty(OutputKeys.INDENT, "yes"); - xform.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); - xform.transform(new DOMSource(xmlDocument), new StreamResult(buf)); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, ""); + transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, ""); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - return buf.toString(); + //transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "xbel"); + //transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "xbel"); + //transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + //transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); + //transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + + transformer.transform(new DOMSource(xmlDocument), new StreamResult(buf)); + + var content = buf.toString(); + content = content.replaceFirst("\\s*standalone=\"(yes|no)\"", ""); + content = content.replace("",""); + + return content; } catch (Exception e) { throw new QuickAccessServiceException("Failed to read document into string", e); diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java index 5eb2054..6f9c501 100644 --- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java +++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java @@ -131,6 +131,34 @@ public void invalidCharactersInCaptionShouldBeEscaped(@TempDir Path tmpdir) { 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 = """ + + + + + + """.replaceAll("[\\r\\n\\t]", ""); + + + assertDoesNotThrow(() -> { + + new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), "my-caption"); + + }); + + var file = Files.readString(pathToDoc).replaceAll("[\\r\\n\\t]", ""); + + assertEquals(file.substring(0,rootObject.length()), rootObject); + } + + private Path loadResourceToDir(String source, Path targetDir) { try (var stream = this.getClass().getClassLoader().getResourceAsStream(source)) { From 42c15bde85369d9f71327086534260ab6a89210a Mon Sep 17 00:00:00 2001 From: jrauber Date: Mon, 16 Jun 2025 20:28:49 +0200 Subject: [PATCH 03/12] Fix issues mentioned by th AI checks --- .idea/developer-tools.xml | 6 --- .../linux/quickaccess/DolphinPlaces.java | 48 ++++++++----------- .../FileConfiguredQuickAccess.java | 5 ++ .../linux/quickaccess/DolphinPlacesTest.java | 2 +- 4 files changed, 27 insertions(+), 34 deletions(-) delete mode 100644 .idea/developer-tools.xml diff --git a/.idea/developer-tools.xml b/.idea/developer-tools.xml deleted file mode 100644 index 01cefa5..0000000 --- a/.idea/developer-tools.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 3e56025..8672f80 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -11,6 +11,7 @@ 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; @@ -18,14 +19,13 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; -import javax.xml.transform.Source; import javax.xml.transform.Transformer; 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.Schema; import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; @@ -59,19 +59,14 @@ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAcc private static final String CONFIG_FILE_NAME = "user-places.xbel"; private static final Path PLACES_FILE = Path.of(HOME_DIR,CONFIG_PATH_IN_HOME, CONFIG_FILE_NAME); - private static final Validator XML_VALIDATOR; + private static final Schema XBEL_SCHEMA; static { try (var schemaDefinition = DolphinPlaces.class.getResourceAsStream("/xbel-1.0.xsd")) { SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - - //factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - //factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - - Source schemaFile = new StreamSource(schemaDefinition); - XML_VALIDATOR = factory.newSchema(schemaFile).newValidator(); + XBEL_SCHEMA = factory.newSchema(new StreamSource(schemaDefinition)); } catch (IOException | SAXException e) { throw new IllegalStateException("Failed to load included XBEL schema definition file.", e); @@ -94,11 +89,11 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) String id = UUID.randomUUID().toString(); + var validator = XBEL_SCHEMA.newValidator(); + LOG.trace("Adding bookmark for target: '{}', displayName: '{}', id: '{}'", target, displayName, id); - // Validate the existing config before modifying it, if it is invalid - // we should not modify it. - XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); + validator.validate(new StreamSource(new StringReader(config))); Document xmlDocument = loadXmlDocument(config); @@ -108,7 +103,7 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) createBookmark(target, displayName, id, xmlDocument); - XML_VALIDATOR.validate(new DOMSource(xmlDocument)); + validator.validate(new StreamSource(new StringReader(config))); return new EntryAndConfig(new DolphinPlacesEntry(id), documentToString(xmlDocument)); @@ -177,17 +172,17 @@ private Document loadXmlDocument(String config) throws QuickAccessServiceExcepti DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - //builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - //builderFactory.setXIncludeAware(false); - //builderFactory.setExpandEntityReferences(false); - //builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - //builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); - //builderFactory.setNamespaceAware(true); + builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + builderFactory.setXIncludeAware(false); + builderFactory.setExpandEntityReferences(false); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + builderFactory.setNamespaceAware(true); DocumentBuilder builder = builderFactory.newDocumentBuilder(); // Prevent external entities from being resolved - //builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); + builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); return builder.parse(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))); @@ -207,11 +202,9 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, ""); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - //transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "xbel"); - //transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "xbel"); - //transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - //transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); - //transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); transformer.transform(new DOMSource(xmlDocument), new StreamResult(buf)); @@ -300,8 +293,9 @@ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implemen public String removeEntryFromConfig(String config) throws QuickAccessServiceException { try { + var validator = XBEL_SCHEMA.newValidator(); - XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); + validator.validate(new StreamSource(new StringReader(config))); Document xmlDocument = loadXmlDocument(config); @@ -309,7 +303,7 @@ public String removeEntryFromConfig(String config) throws QuickAccessServiceExce removeStaleBookmarks(nodeList); - XML_VALIDATOR.validate(new DOMSource(xmlDocument)); + validator.validate(new StreamSource(new StringReader(config))); return documentToString(xmlDocument); diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java index 6f2dc26..6b9027d 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java @@ -44,8 +44,13 @@ abstract class FileConfiguredQuickAccess implements QuickAccessService { 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(); } catch (IOException e) { throw new QuickAccessServiceException("Failed to add entry to %s.".formatted(configFile), e); diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java index 6f9c501..19b19ae 100644 --- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java +++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java @@ -155,7 +155,7 @@ public void xmlFileRootObjectShouldNotBeChangedWhenAddingAnEntry(@TempDir Path t var file = Files.readString(pathToDoc).replaceAll("[\\r\\n\\t]", ""); - assertEquals(file.substring(0,rootObject.length()), rootObject); + assertEquals(file.substring(0,rootObject.length()), rootObject, "Root object of the XML file should not be changed when adding an entry"); } From b215b478c80b5460a5b06319e18cba121e9803a0 Mon Sep 17 00:00:00 2001 From: jrauber Date: Mon, 16 Jun 2025 20:31:50 +0200 Subject: [PATCH 04/12] Fix issues mentioned by th AI checks --- .../java/org/cryptomator/linux/quickaccess/DolphinPlaces.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 8672f80..b39aefc 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -209,8 +209,9 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE transformer.transform(new DOMSource(xmlDocument), new StreamResult(buf)); var content = buf.toString(); + content = content.replaceFirst("\\s*standalone=\"(yes|no)\"", ""); - content = content.replace("",""); + content = content.replaceFirst("",""); return content; From ee1d5f2f6b0d00a3038c34a6748dd79533e8f602 Mon Sep 17 00:00:00 2001 From: jrauber Date: Mon, 16 Jun 2025 20:37:47 +0200 Subject: [PATCH 05/12] Fix issues mentioned by th AI checks --- .../cryptomator/linux/quickaccess/DolphinPlaces.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index b39aefc..d510004 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -103,9 +103,11 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) createBookmark(target, displayName, id, xmlDocument); - validator.validate(new StreamSource(new StringReader(config))); + var changedConfig = documentToString(xmlDocument); + + validator.validate(new StreamSource(new StringReader(changedConfig))); - return new EntryAndConfig(new DolphinPlacesEntry(id), documentToString(xmlDocument)); + return new EntryAndConfig(new DolphinPlacesEntry(id), changedConfig); } catch (SAXException e) { throw new QuickAccessServiceException("Invalid structure in xbel bookmark file", e); @@ -304,9 +306,11 @@ public String removeEntryFromConfig(String config) throws QuickAccessServiceExce removeStaleBookmarks(nodeList); - validator.validate(new StreamSource(new StringReader(config))); + var changedConfig = documentToString(xmlDocument); + + validator.validate(new StreamSource(new StringReader(changedConfig))); - return documentToString(xmlDocument); + return changedConfig; } catch (IOException | SAXException | IllegalStateException e) { throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e); From 48e59765c9d6b821bbc208fba932845f53873931 Mon Sep 17 00:00:00 2001 From: jrauber Date: Mon, 16 Jun 2025 20:41:06 +0200 Subject: [PATCH 06/12] Fix issues mentioned by th AI checks --- .../java/org/cryptomator/linux/quickaccess/DolphinPlaces.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index d510004..f8f315b 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -206,7 +206,6 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); - transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); transformer.transform(new DOMSource(xmlDocument), new StreamResult(buf)); From 32522a49dc9ee905f5e9ded8da45457f701d7789 Mon Sep 17 00:00:00 2001 From: jrauber Date: Tue, 17 Jun 2025 19:15:15 +0200 Subject: [PATCH 07/12] Adjust coding style to the existing parts of the project - removed line-breaks - increased usage of 'var' --- .../linux/quickaccess/DolphinPlaces.java | 98 +++---------------- .../FileConfiguredQuickAccess.java | 4 - .../linux/quickaccess/DolphinPlacesTest.java | 50 +--------- 3 files changed, 16 insertions(+), 136 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index f8f315b..c7e9cff 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -62,12 +62,9 @@ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAcc private static final Schema XBEL_SCHEMA; static { - try (var schemaDefinition = DolphinPlaces.class.getResourceAsStream("/xbel-1.0.xsd")) { - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); XBEL_SCHEMA = factory.newSchema(new StreamSource(schemaDefinition)); - } catch (IOException | SAXException e) { throw new IllegalStateException("Failed to load included XBEL schema definition file.", e); } @@ -86,29 +83,17 @@ public DolphinPlaces(Path configFilePath) { EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { try { - - String id = UUID.randomUUID().toString(); - var validator = XBEL_SCHEMA.newValidator(); - + var id = UUID.randomUUID().toString(); LOG.trace("Adding bookmark for target: '{}', displayName: '{}', id: '{}'", target, displayName, id); - validator.validate(new StreamSource(new StringReader(config))); - - Document xmlDocument = loadXmlDocument(config); - - NodeList nodeList = extractBookmarksByPath(target, xmlDocument); - + var xmlDocument = loadXmlDocument(config); + var nodeList = extractBookmarksByPath(target, xmlDocument); removeStaleBookmarks(nodeList); - createBookmark(target, displayName, id, xmlDocument); - var changedConfig = documentToString(xmlDocument); - validator.validate(new StreamSource(new StringReader(changedConfig))); - return new EntryAndConfig(new DolphinPlacesEntry(id), changedConfig); - } catch (SAXException e) { throw new QuickAccessServiceException("Invalid structure in xbel bookmark file", e); } catch (IOException e) { @@ -117,7 +102,6 @@ EntryAndConfig addEntryToConfig(String config, Path target, String displayName) } private void removeStaleBookmarks(NodeList nodeList) { - for (int i = nodeList.getLength() - 1; i >= 0; i--) { Node node = nodeList.item(i); node.getParentNode().removeChild(node); @@ -125,97 +109,65 @@ private void removeStaleBookmarks(NodeList nodeList) { } private NodeList extractBookmarksByPath(Path target, Document xmlDocument) throws QuickAccessServiceException { - try { - - XPathFactory xpathFactory = XPathFactory.newInstance(); - XPath xpath = xpathFactory.newXPath(); - - SimpleVariableResolver variableResolver = new SimpleVariableResolver(); - + var xpathFactory = XPathFactory.newInstance(); + var xpath = xpathFactory.newXPath(); + var variableResolver = new SimpleVariableResolver(); variableResolver.addVariable(new QName("uri"), target.toUri().toString()); - xpath.setXPathVariableResolver(variableResolver); - - String expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][@href=$uri]"; - + var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][@href=$uri]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - } catch (Exception e) { throw new QuickAccessServiceException("Failed to extract bookmarks by path", e); } } private NodeList extractBookmarksById(String id, Document xmlDocument) throws QuickAccessServiceException { - try { - - XPathFactory xpathFactory = XPathFactory.newInstance(); - XPath xpath = xpathFactory.newXPath(); - - SimpleVariableResolver variableResolver = new SimpleVariableResolver(); - + var xpathFactory = XPathFactory.newInstance(); + var xpath = xpathFactory.newXPath(); + var variableResolver = new SimpleVariableResolver(); variableResolver.addVariable(new QName("id"), id); - xpath.setXPathVariableResolver(variableResolver); - - String expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][info/metadata/id[text()=$id]]"; - + var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][info/metadata/id[text()=$id]]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - } catch (Exception e) { throw new QuickAccessServiceException("Failed to extract bookmarks by id", e); } } private Document loadXmlDocument(String config) throws QuickAccessServiceException { - try { - - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - + var builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); builderFactory.setXIncludeAware(false); builderFactory.setExpandEntityReferences(false); builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); builderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); builderFactory.setNamespaceAware(true); - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - // Prevent external entities from being resolved builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); - return builder.parse(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))); - } catch (Exception e) { throw new QuickAccessServiceException("Failed to parse the xbel bookmark file", e); } } private String documentToString(Document xmlDocument) throws QuickAccessServiceException { - try { - - StringWriter buf = new StringWriter(); - + var buf = new StringWriter(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, ""); transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, ""); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name()); - transformer.transform(new DOMSource(xmlDocument), new StreamResult(buf)); - var content = buf.toString(); - content = content.replaceFirst("\\s*standalone=\"(yes|no)\"", ""); content = content.replaceFirst("",""); - return content; - } catch (Exception e) { throw new QuickAccessServiceException("Failed to read document into string", e); } @@ -246,7 +198,6 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE * @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"); @@ -255,29 +206,19 @@ private void createBookmark(Path target, String displayName, String id, Document 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 (Exception e) { throw new QuickAccessServiceException("Failed to insert bookmark for target: " + target, e); } @@ -293,24 +234,15 @@ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implemen @Override public String removeEntryFromConfig(String config) throws QuickAccessServiceException { - try { var validator = XBEL_SCHEMA.newValidator(); - validator.validate(new StreamSource(new StringReader(config))); - - Document xmlDocument = loadXmlDocument(config); - - NodeList nodeList = extractBookmarksById(id, xmlDocument); - + var xmlDocument = loadXmlDocument(config); + var nodeList = extractBookmarksById(id, xmlDocument); removeStaleBookmarks(nodeList); - var changedConfig = documentToString(xmlDocument); - validator.validate(new StreamSource(new StringReader(changedConfig))); - return changedConfig; - } catch (IOException | SAXException | IllegalStateException e) { throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e); } diff --git a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java index 6b9027d..44d74fd 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/FileConfiguredQuickAccess.java @@ -44,13 +44,9 @@ abstract class FileConfiguredQuickAccess implements QuickAccessService { 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(); } catch (IOException e) { throw new QuickAccessServiceException("Failed to add entry to %s.".formatted(configFile), e); diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java index 19b19ae..79741ef 100644 --- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java +++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java @@ -35,17 +35,12 @@ public void testInit() { 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).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(); }); } @@ -53,44 +48,29 @@ public void addingAnIdenticalEntryShouldLeadToReplacementOfExistingEntry(@TempDi @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).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).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)); } @@ -98,18 +78,12 @@ public void addingShouldNotReplaceIfFileIsNotValid(@TempDir Path tmpdir) { @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).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)); } @@ -117,26 +91,18 @@ public void addingShouldNotReplaceIfFileIsNotWellFormed(@TempDir Path tmpdir) { @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).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 = """ @@ -145,43 +111,30 @@ public void xmlFileRootObjectShouldNotBeChangedWhenAddingAnEntry(@TempDir Path t xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info"> """.replaceAll("[\\r\\n\\t]", ""); - - assertDoesNotThrow(() -> { - new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), "my-caption"); - }); - var file = Files.readString(pathToDoc).replaceAll("[\\r\\n\\t]", ""); - - assertEquals(file.substring(0,rootObject.length()), rootObject, "Root object of the XML file should not be changed when adding an entry"); + assertEquals(rootObject, file.substring(0,rootObject.length()), "Root object of the XML file should not be changed when adding an entry"); } private Path loadResourceToDir(String source, Path targetDir) { - try (var stream = this.getClass().getClassLoader().getResourceAsStream(source)) { - if (stream == null) { throw new IOException("Resource not found: " + source); } - Files.copy(stream, targetDir.resolve("user-places.xbel"), StandardCopyOption.REPLACE_EXISTING); - return targetDir.resolve("user-places.xbel"); - } catch (IOException e) { throw new RuntimeException("Failed to load resource: " + source, e); } } private String loadFile(Path file) { - if (!Files.exists(file)) { throw new RuntimeException("File does not exist: " + file); } - try { return Files.readString(file); } catch (IOException e) { @@ -197,7 +150,6 @@ private int countOccurrences(String content, String searchString) { count++; index += searchString.length(); } - return count; } } From 700fc498e8e94204102702f941046cc4a83a8c3c Mon Sep 17 00:00:00 2001 From: jrauber Date: Tue, 15 Jul 2025 21:59:31 +0200 Subject: [PATCH 08/12] Fix some of the review remarks --- .../linux/quickaccess/DolphinPlaces.java | 28 ++++++++----------- .../linux/quickaccess/DolphinPlacesTest.java | 12 ++++---- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index c7e9cff..b535cfa 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -19,14 +19,14 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; import javax.xml.transform.Transformer; 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.Schema; import javax.xml.validation.SchemaFactory; -import javax.xml.xpath.XPath; +import javax.xml.validation.Validator; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathVariableResolver; @@ -54,17 +54,15 @@ public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAcc 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 String HOME_DIR = System.getProperty("user.home"); - private static final String CONFIG_PATH_IN_HOME = ".local/share"; - private static final String CONFIG_FILE_NAME = "user-places.xbel"; - private static final Path PLACES_FILE = Path.of(HOME_DIR,CONFIG_PATH_IN_HOME, CONFIG_FILE_NAME); + private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel"); - private static final Schema XBEL_SCHEMA; + private static final Validator XML_VALIDATOR; static { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); try (var schemaDefinition = DolphinPlaces.class.getResourceAsStream("/xbel-1.0.xsd")) { - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - XBEL_SCHEMA = factory.newSchema(new StreamSource(schemaDefinition)); + Source schemaFile = new StreamSource(schemaDefinition); + XML_VALIDATOR = factory.newSchema(schemaFile).newValidator(); } catch (IOException | SAXException e) { throw new IllegalStateException("Failed to load included XBEL schema definition file.", e); } @@ -76,23 +74,22 @@ public DolphinPlaces() { } public DolphinPlaces(Path configFilePath) { - super(configFilePath.resolve(CONFIG_FILE_NAME), MAX_FILE_SIZE); + super(configFilePath, MAX_FILE_SIZE); } @Override EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { try { - var validator = XBEL_SCHEMA.newValidator(); var id = UUID.randomUUID().toString(); LOG.trace("Adding bookmark for target: '{}', displayName: '{}', id: '{}'", target, displayName, id); - validator.validate(new StreamSource(new StringReader(config))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); var xmlDocument = loadXmlDocument(config); var nodeList = extractBookmarksByPath(target, xmlDocument); removeStaleBookmarks(nodeList); createBookmark(target, displayName, id, xmlDocument); var changedConfig = documentToString(xmlDocument); - validator.validate(new StreamSource(new StringReader(changedConfig))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(changedConfig))); return new EntryAndConfig(new DolphinPlacesEntry(id), changedConfig); } catch (SAXException e) { throw new QuickAccessServiceException("Invalid structure in xbel bookmark file", e); @@ -235,13 +232,12 @@ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implemen @Override public String removeEntryFromConfig(String config) throws QuickAccessServiceException { try { - var validator = XBEL_SCHEMA.newValidator(); - validator.validate(new StreamSource(new StringReader(config))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); var xmlDocument = loadXmlDocument(config); var nodeList = extractBookmarksById(id, xmlDocument); removeStaleBookmarks(nodeList); var changedConfig = documentToString(xmlDocument); - validator.validate(new StreamSource(new StringReader(changedConfig))); + XML_VALIDATOR.validate(new StreamSource(new StringReader(changedConfig))); return changedConfig; } catch (IOException | SAXException | IllegalStateException e) { throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e); diff --git a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java index 79741ef..7400c6f 100644 --- a/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java +++ b/src/test/java/org/cryptomator/linux/quickaccess/DolphinPlacesTest.java @@ -38,7 +38,7 @@ public void addingAnIdenticalEntryShouldLeadToReplacementOfExistingEntry(@TempDi assertTrue(loadFile(pathToDoc).contains(UUID_FOLDER_1)); assertTrue(loadFile(pathToDoc).contains(CAPTION_FOLDER_1)); assertDoesNotThrow(() -> { - var entry = new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1); + 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(); @@ -53,7 +53,7 @@ public void addingAnIdenticalEntryShouldLeadToReplacementOfMultipleExistingEntry assertEquals(1, countOccurrences(loadFile(pathToDoc),UUID_FOLDER_1_IDENTICAL)); assertEquals(2, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1)); assertDoesNotThrow(() -> { - var entry = new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1); + 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)); @@ -69,7 +69,7 @@ public void addingShouldNotReplaceIfFileIsNotValid(@TempDir Path tmpdir) { assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1)); assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1)); assertThrows(QuickAccessServiceException.class, () -> { - new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1); + 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)); @@ -82,7 +82,7 @@ public void addingShouldNotReplaceIfFileIsNotWellFormed(@TempDir Path tmpdir) { assertEquals(1, countOccurrences(loadFile(pathToDoc), UUID_FOLDER_1)); assertEquals(1, countOccurrences(loadFile(pathToDoc), CAPTION_FOLDER_1)); assertThrows(QuickAccessServiceException.class, () -> { - new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), CAPTION_FOLDER_1); + 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)); @@ -94,7 +94,7 @@ public void invalidCharactersInCaptionShouldBeEscaped(@TempDir Path tmpdir) { var pathToDoc = loadResourceToDir(RESOURCE_USER_PLACES, tmpdir); assertEquals(0, countOccurrences(loadFile(pathToDoc), "< & >")); assertDoesNotThrow(() -> { - new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), "< & >"); + new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), "< & >"); }); assertEquals(1, countOccurrences(loadFile(pathToDoc), "< & >")); } @@ -112,7 +112,7 @@ public void xmlFileRootObjectShouldNotBeChangedWhenAddingAnEntry(@TempDir Path t """.replaceAll("[\\r\\n\\t]", ""); assertDoesNotThrow(() -> { - new DolphinPlaces(tmpdir).add(Path.of(PATH_FOLDER_1), "my-caption"); + new DolphinPlaces(tmpdir.resolve("user-places.xbel")).add(Path.of(PATH_FOLDER_1), "my-caption"); }); var file = Files.readString(pathToDoc).replaceAll("[\\r\\n\\t]", ""); assertEquals(rootObject, file.substring(0,rootObject.length()), "Root object of the XML file should not be changed when adding an entry"); From 743b00ff29d818bb77c5428d3068f5c5a984014c Mon Sep 17 00:00:00 2001 From: jrauber Date: Wed, 16 Jul 2025 20:37:59 +0200 Subject: [PATCH 09/12] Add exceptions --- .../linux/quickaccess/DolphinPlaces.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index b535cfa..1c37de7 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -8,6 +8,7 @@ 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; @@ -18,9 +19,11 @@ 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; @@ -28,6 +31,7 @@ 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 javax.xml.xpath.XPathVariableResolver; import java.io.ByteArrayInputStream; @@ -114,8 +118,8 @@ private NodeList extractBookmarksByPath(Path target, Document xmlDocument) throw xpath.setXPathVariableResolver(variableResolver); var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][@href=$uri]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - } catch (Exception e) { - throw new QuickAccessServiceException("Failed to extract bookmarks by path", e); + } catch (XPathExpressionException xee) { + throw new QuickAccessServiceException("Invalid XPath expression", xee); } } @@ -128,8 +132,8 @@ private NodeList extractBookmarksById(String id, Document xmlDocument) throws Qu xpath.setXPathVariableResolver(variableResolver); var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][info/metadata/id[text()=$id]]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); - } catch (Exception e) { - throw new QuickAccessServiceException("Failed to extract bookmarks by id", e); + } catch (XPathExpressionException xee) { + throw new QuickAccessServiceException("Invalid XPath expression", xee); } } @@ -146,8 +150,8 @@ private Document loadXmlDocument(String config) throws QuickAccessServiceExcepti // Prevent external entities from being resolved builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); return builder.parse(new ByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8))); - } catch (Exception e) { - throw new QuickAccessServiceException("Failed to parse the xbel bookmark file", e); + } catch (IOException | SAXException | ParserConfigurationException e) { + throw new QuickAccessServiceException("Error while loading xml file", e); } } @@ -165,8 +169,8 @@ private String documentToString(Document xmlDocument) throws QuickAccessServiceE content = content.replaceFirst("\\s*standalone=\"(yes|no)\"", ""); content = content.replaceFirst("",""); return content; - } catch (Exception e) { - throw new QuickAccessServiceException("Failed to read document into string", e); + } catch (TransformerException e) { + throw new QuickAccessServiceException("Error while serializing document to string", e); } } @@ -216,8 +220,8 @@ private void createBookmark(Path target, String displayName, String id, Document metadataOwner.setAttribute("owner", "https://cryptomator.org"); idElem.setTextContent(id); xmlDocument.getDocumentElement().appendChild(bookmark); - } catch (Exception e) { - throw new QuickAccessServiceException("Failed to insert bookmark for target: " + target, e); + } catch (DOMException | IllegalArgumentException e) { + throw new QuickAccessServiceException("Error while creating bookmark for target: " + target, e); } } @@ -277,4 +281,4 @@ public Object resolveVariable(QName variableName) { public static boolean isSupported() { return Files.exists(PLACES_FILE); } -} +} \ No newline at end of file From f65d2c4e834b8752a969137d0df1c79177ba90bb Mon Sep 17 00:00:00 2001 From: jrauber Date: Wed, 16 Jul 2025 20:41:09 +0200 Subject: [PATCH 10/12] Add exceptions --- .../java/org/cryptomator/linux/quickaccess/DolphinPlaces.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 1c37de7..1588d4a 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -83,7 +83,6 @@ public DolphinPlaces(Path configFilePath) { @Override EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException { - try { var id = UUID.randomUUID().toString(); LOG.trace("Adding bookmark for target: '{}', displayName: '{}', id: '{}'", target, displayName, id); From 62ae224dc148cbc992fde9e7f060e44e8f34eeec Mon Sep 17 00:00:00 2001 From: jrauber Date: Thu, 17 Jul 2025 22:10:59 +0200 Subject: [PATCH 11/12] make private class static --- .../java/org/cryptomator/linux/quickaccess/DolphinPlaces.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index 1588d4a..b905920 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -251,7 +251,7 @@ public String removeEntryFromConfig(String config) throws QuickAccessServiceExce /** * Resolver in order to define parameter for XPATH expression. */ - private class SimpleVariableResolver implements XPathVariableResolver { + private static class SimpleVariableResolver implements XPathVariableResolver { private final Map vars = new HashMap<>(); From 3943ef2e9780a52590ff02ddddf34ee4f7f0b654 Mon Sep 17 00:00:00 2001 From: jrauber Date: Tue, 22 Jul 2025 20:38:25 +0200 Subject: [PATCH 12/12] - Simplified implementation by removing the SimpleVariableResolver and instead providing a lambda into the xpath - Removed the unnecessary validation before deleting --- .../linux/quickaccess/DolphinPlaces.java | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java index b905920..22cf207 100644 --- a/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java +++ b/src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java @@ -33,7 +33,6 @@ import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import javax.xml.xpath.XPathVariableResolver; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; @@ -41,8 +40,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -112,9 +109,12 @@ private NodeList extractBookmarksByPath(Path target, Document xmlDocument) throw try { var xpathFactory = XPathFactory.newInstance(); var xpath = xpathFactory.newXPath(); - var variableResolver = new SimpleVariableResolver(); - variableResolver.addVariable(new QName("uri"), target.toUri().toString()); - xpath.setXPathVariableResolver(variableResolver); + xpath.setXPathVariableResolver(v -> { + if (v.equals(new QName("uri"))) { + return target.toUri().toString(); + } + throw new IllegalArgumentException(); + }); var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][@href=$uri]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); } catch (XPathExpressionException xee) { @@ -126,9 +126,12 @@ private NodeList extractBookmarksById(String id, Document xmlDocument) throws Qu try { var xpathFactory = XPathFactory.newInstance(); var xpath = xpathFactory.newXPath(); - var variableResolver = new SimpleVariableResolver(); - variableResolver.addVariable(new QName("id"), id); - xpath.setXPathVariableResolver(variableResolver); + xpath.setXPathVariableResolver(v -> { + if (v.equals(new QName("id"))) { + return id; + } + throw new IllegalArgumentException(); + }); var expression = "/xbel/bookmark[info/metadata[@owner='https://cryptomator.org']][info/metadata/id[text()=$id]]"; return (NodeList) xpath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET); } catch (XPathExpressionException xee) { @@ -235,7 +238,6 @@ private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implemen @Override public String removeEntryFromConfig(String config) throws QuickAccessServiceException { try { - XML_VALIDATOR.validate(new StreamSource(new StringReader(config))); var xmlDocument = loadXmlDocument(config); var nodeList = extractBookmarksById(id, xmlDocument); removeStaleBookmarks(nodeList); @@ -248,34 +250,6 @@ public String removeEntryFromConfig(String config) throws QuickAccessServiceExce } } - /** - * Resolver in order to define parameter for XPATH expression. - */ - private static class SimpleVariableResolver implements XPathVariableResolver { - - private final Map vars = new HashMap<>(); - - /** - * Adds a variable to the resolver. - * - * @param name The name of the variable - * @param value The value of the variable - */ - public void addVariable(QName name, Object value) { - vars.put(name, value); - } - - /** - * Resolves a variable by its name. - * - * @param variableName The name of the variable to resolve - * @return The value of the variable, or null if not found - */ - public Object resolveVariable(QName variableName) { - return vars.get(variableName); - } - } - @CheckAvailability public static boolean isSupported() { return Files.exists(PLACES_FILE);