Skip to content

Commit 491b93f

Browse files
committed
Merge branch 'release/1.5.2'
2 parents adbf840 + 54f12d6 commit 491b93f

File tree

4 files changed

+172
-116
lines changed

4 files changed

+172
-116
lines changed

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>org.cryptomator</groupId>
77
<artifactId>integrations-linux</artifactId>
8-
<version>1.5.1</version>
8+
<version>1.5.2</version>
99

1010
<name>integrations-linux</name>
1111
<description>Provides optional Linux services used by Cryptomator</description>
@@ -47,17 +47,17 @@
4747
<appindicator.version>1.4.1</appindicator.version>
4848

4949
<!-- test dependencies -->
50-
<junit.version>5.11.3</junit.version>
50+
<junit.version>5.11.4</junit.version>
5151

5252
<!-- build plugin dependencies -->
5353
<mvn-compiler.version>3.13.0</mvn-compiler.version>
54-
<mvn-surefire.version>3.5.1</mvn-surefire.version>
54+
<mvn-surefire.version>3.5.2</mvn-surefire.version>
5555
<mvn-enforcer.version>3.5.0</mvn-enforcer.version>
5656
<mvn-source.version>3.3.1</mvn-source.version>
57-
<mvn-javadoc.version>3.10.1</mvn-javadoc.version>
57+
<mvn-javadoc.version>3.11.2</mvn-javadoc.version>
5858
<mvn-gpg.version>3.2.7</mvn-gpg.version>
5959
<mvn-deploy.version>3.1.3</mvn-deploy.version>
60-
<dependency-check.version>11.1.0</dependency-check.version>
60+
<dependency-check.version>11.1.1</dependency-check.version>
6161
<nexus-staging.version>1.7.0</nexus-staging.version>
6262
</properties>
6363

src/main/java/org/cryptomator/linux/quickaccess/DolphinPlaces.java

Lines changed: 41 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@
1515
import javax.xml.validation.Validator;
1616
import java.io.IOException;
1717
import java.io.StringReader;
18-
import java.nio.charset.StandardCharsets;
1918
import java.nio.file.Files;
2019
import java.nio.file.Path;
21-
import java.nio.file.StandardCopyOption;
22-
import java.nio.file.StandardOpenOption;
2320
import java.util.List;
2421
import java.util.UUID;
25-
import java.util.concurrent.locks.Lock;
26-
import java.util.concurrent.locks.ReentrantLock;
2722

2823
/**
2924
* Implemenation of the {@link QuickAccessService} for KDE desktop environments using Dolphin file browser.
@@ -32,12 +27,10 @@
3227
@CheckAvailability
3328
@OperatingSystem(OperatingSystem.Value.LINUX)
3429
@Priority(90)
35-
public class DolphinPlaces implements QuickAccessService {
30+
public class DolphinPlaces extends FileConfiguredQuickAccess implements QuickAccessService {
3631

37-
private static final int MAX_FILE_SIZE = 1 << 15; //xml is quite verbose
32+
private static final int MAX_FILE_SIZE = 1 << 20; //1MiB, xml is quite verbose
3833
private static final Path PLACES_FILE = Path.of(System.getProperty("user.home"), ".local/share/user-places.xbel");
39-
private static final Path TMP_FILE = Path.of(System.getProperty("java.io.tmpdir"), "user-places.xbel.cryptomator.tmp");
40-
private static final Lock MODIFY_LOCK = new ReentrantLock();
4134
private static final String ENTRY_TEMPLATE = """
4235
<bookmark href=\"%s\">
4336
<title>%s</title>
@@ -51,7 +44,6 @@ public class DolphinPlaces implements QuickAccessService {
5144
</info>
5245
</bookmark>""";
5346

54-
5547
private static final Validator XML_VALIDATOR;
5648

5749
static {
@@ -64,96 +56,82 @@ public class DolphinPlaces implements QuickAccessService {
6456
}
6557
}
6658

59+
//SPI constructor
60+
public DolphinPlaces() {
61+
super(PLACES_FILE, MAX_FILE_SIZE);
62+
}
6763

6864
@Override
69-
public QuickAccessService.QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
70-
String id = UUID.randomUUID().toString();
65+
EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException {
7166
try {
72-
MODIFY_LOCK.lock();
73-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
74-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
75-
}
76-
var placesContent = Files.readString(PLACES_FILE);
67+
String id = UUID.randomUUID().toString();
7768
//validate
78-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
69+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
7970
// modify
80-
int insertIndex = placesContent.lastIndexOf("</xbel"); //cannot be -1 due to validation; we do not match the end tag, since betweent tag name and closing bracket can be whitespaces
81-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
82-
writer.write(placesContent, 0, insertIndex);
83-
writer.newLine();
84-
writer.write(ENTRY_TEMPLATE.formatted(target.toUri(), displayName, id).indent(1));
85-
writer.newLine();
86-
writer.write(placesContent, insertIndex, placesContent.length() - insertIndex);
87-
}
88-
// save
89-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
90-
return new DolphinPlacesEntry(id);
71+
int insertIndex = config.lastIndexOf("</xbel"); //cannot be -1 due to validation; we do not match the whole end tag, since between tag name and closing bracket can be whitespaces
72+
var adjustedConfig = config.substring(0, insertIndex) //
73+
+ "\n" //
74+
+ ENTRY_TEMPLATE.formatted(target.toUri(), escapeXML(displayName), id).indent(1) //
75+
+ "\n" //
76+
+ config.substring(insertIndex);
77+
return new EntryAndConfig(new DolphinPlacesEntry(id), adjustedConfig);
9178
} catch (SAXException | IOException e) {
9279
throw new QuickAccessServiceException("Adding entry to KDE places file failed.", e);
93-
} finally {
94-
MODIFY_LOCK.unlock();
9580
}
9681
}
9782

98-
private static class DolphinPlacesEntry implements QuickAccessEntry {
83+
private String escapeXML(String s) {
84+
return s.replace("&","&amp;") //
85+
.replace("<","&lt;") //
86+
.replace(">","&gt;");
87+
}
88+
89+
private class DolphinPlacesEntry extends FileConfiguredQuickAccessEntry implements QuickAccessEntry {
9990

10091
private final String id;
101-
private volatile boolean isRemoved = false;
10292

10393
DolphinPlacesEntry(String id) {
10494
this.id = id;
10595
}
10696

10797
@Override
108-
public void remove() throws QuickAccessServiceException {
98+
public String removeEntryFromConfig(String config) throws QuickAccessServiceException {
10999
try {
110-
MODIFY_LOCK.lock();
111-
if (isRemoved) {
112-
return;
113-
}
114-
if (Files.size(PLACES_FILE) > MAX_FILE_SIZE) {
115-
throw new IOException("File %s exceeds size of %d bytes".formatted(PLACES_FILE, MAX_FILE_SIZE));
116-
}
117-
var placesContent = Files.readString(PLACES_FILE);
118-
int idIndex = placesContent.lastIndexOf(id);
100+
int idIndex = config.lastIndexOf(id);
119101
if (idIndex == -1) {
120-
isRemoved = true;
121-
return; //we assume someone has removed our entry
102+
return config; //assume someone has removed our entry, nothing to do
122103
}
123104
//validate
124-
XML_VALIDATOR.validate(new StreamSource(new StringReader(placesContent)));
105+
XML_VALIDATOR.validate(new StreamSource(new StringReader(config)));
125106
//modify
126-
int openingTagIndex = indexOfEntryOpeningTag(placesContent, idIndex);
127-
var contentToWrite1 = placesContent.substring(0, openingTagIndex).stripTrailing();
107+
int openingTagIndex = indexOfEntryOpeningTag(config, idIndex);
108+
var contentToWrite1 = config.substring(0, openingTagIndex).stripTrailing();
128109

129-
int closingTagEndIndex = placesContent.indexOf('>', placesContent.indexOf("</bookmark", idIndex));
130-
var part2Tmp = placesContent.substring(closingTagEndIndex + 1).split("\\A\\v+", 2); //removing leading vertical whitespaces, but no indentation
110+
int closingTagEndIndex = config.indexOf('>', config.indexOf("</bookmark", idIndex));
111+
var part2Tmp = config.substring(closingTagEndIndex + 1).split("\\A\\v+", 2); //removing leading vertical whitespaces, but no indentation
131112
var contentToWrite2 = part2Tmp[part2Tmp.length - 1];
132113

133-
try (var writer = Files.newBufferedWriter(TMP_FILE, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
134-
writer.write(contentToWrite1);
135-
writer.newLine();
136-
writer.write(contentToWrite2);
137-
}
138-
// save
139-
Files.move(TMP_FILE, PLACES_FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
140-
isRemoved = true;
141-
} catch (IOException | SAXException e) {
114+
return contentToWrite1 + "\n" + contentToWrite2;
115+
} catch (IOException | SAXException | IllegalStateException e) {
142116
throw new QuickAccessServiceException("Removing entry from KDE places file failed.", e);
143-
} finally {
144-
MODIFY_LOCK.unlock();
145117
}
146118
}
147119

120+
/**
121+
* Returns the start index (inclusive) of the {@link DolphinPlaces#ENTRY_TEMPLATE} entry
122+
* @param placesContent the content of the XBEL places file
123+
* @param idIndex start index (inclusive) of the entrys id tag value
124+
* @return start index of the first bookmark tag, searching backwards from idIndex
125+
*/
148126
private int indexOfEntryOpeningTag(String placesContent, int idIndex) {
149127
var xmlWhitespaceChars = List.of(' ', '\t', '\n');
150128
for (char c : xmlWhitespaceChars) {
151-
int idx = placesContent.lastIndexOf("<bookmark" + c, idIndex);
129+
int idx = placesContent.lastIndexOf("<bookmark" + c, idIndex); //with the whitespace we ensure, that no tags starting with "bookmark" (e.g. bookmarkz) are selected
152130
if (idx != -1) {
153131
return idx;
154132
}
155133
}
156-
throw new IllegalStateException("File " + PLACES_FILE + " is valid xbel file, but does not contain opening bookmark tag.");
134+
throw new IllegalStateException("Found entry id " + id + " in " + PLACES_FILE + ", but it is not a child of <bookmark> tag.");
157135
}
158136
}
159137

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.cryptomator.linux.quickaccess;
2+
3+
import org.cryptomator.integrations.quickaccess.QuickAccessService;
4+
import org.cryptomator.integrations.quickaccess.QuickAccessServiceException;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.nio.file.AtomicMoveNotSupportedException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.StandardCopyOption;
14+
import java.nio.file.StandardOpenOption;
15+
import java.util.concurrent.locks.Lock;
16+
import java.util.concurrent.locks.ReentrantReadWriteLock;
17+
18+
abstract class FileConfiguredQuickAccess implements QuickAccessService {
19+
20+
private static final Logger LOG = LoggerFactory.getLogger(FileConfiguredQuickAccess.class);
21+
22+
private final int maxFileSize;
23+
private final Path configFile;
24+
private final Path tmpFile;
25+
private final Lock modifyLock = new ReentrantReadWriteLock().writeLock();
26+
27+
FileConfiguredQuickAccess(Path configFile, int maxFileSize) {
28+
this.configFile = configFile;
29+
this.maxFileSize = maxFileSize;
30+
this.tmpFile = configFile.resolveSibling("." + configFile.getFileName() + ".cryptomator.tmp");
31+
Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
32+
}
33+
34+
@Override
35+
public QuickAccessEntry add(Path target, String displayName) throws QuickAccessServiceException {
36+
try {
37+
modifyLock.lock();
38+
var entryAndConfig = addEntryToConfig(readConfig(), target, displayName);
39+
persistConfig(entryAndConfig.config());
40+
return entryAndConfig.entry();
41+
} catch (IOException e) {
42+
throw new QuickAccessServiceException("Failed to add entry to %s.".formatted(configFile), e);
43+
} finally {
44+
modifyLock.unlock();
45+
}
46+
}
47+
48+
record EntryAndConfig(FileConfiguredQuickAccessEntry entry, String config) {
49+
}
50+
51+
abstract EntryAndConfig addEntryToConfig(String config, Path target, String displayName) throws QuickAccessServiceException;
52+
53+
54+
protected abstract class FileConfiguredQuickAccessEntry implements QuickAccessEntry {
55+
56+
private volatile boolean isRemoved = false;
57+
58+
@Override
59+
public void remove() throws QuickAccessServiceException {
60+
try {
61+
modifyLock.lock();
62+
if (isRemoved) {
63+
return;
64+
}
65+
checkFileSize();
66+
var config = readConfig();
67+
var adjustedConfig = removeEntryFromConfig(config);
68+
persistConfig(adjustedConfig);
69+
isRemoved = true;
70+
} catch (IOException e) {
71+
throw new QuickAccessServiceException("Failed to remove entry to %s.".formatted(configFile), e);
72+
} finally {
73+
modifyLock.unlock();
74+
}
75+
}
76+
77+
abstract String removeEntryFromConfig(String config) throws QuickAccessServiceException;
78+
}
79+
80+
private String readConfig() throws IOException {
81+
return Files.readString(configFile, StandardCharsets.UTF_8);
82+
}
83+
84+
private void persistConfig(String newConfig) throws IOException {
85+
Files.writeString(tmpFile, newConfig, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
86+
try {
87+
Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
88+
} catch (AtomicMoveNotSupportedException e) {
89+
Files.move(tmpFile, configFile, StandardCopyOption.REPLACE_EXISTING);
90+
}
91+
}
92+
93+
private void checkFileSize() throws IOException {
94+
if (Files.size(configFile) > maxFileSize) {
95+
throw new IOException("File %s exceeds size of %d bytes".formatted(configFile, maxFileSize));
96+
}
97+
}
98+
99+
private void cleanup() {
100+
try {
101+
Files.deleteIfExists(tmpFile);
102+
} catch (IOException e) {
103+
LOG.warn("Unable to delete {}. Need to be deleted manually.", tmpFile);
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)