Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linked files with an absolute path can be opened again #9129

Merged
merged 5 commits into from
Sep 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve

### Fixed

- We fixed an issue where linked fails containing parts of the main file directory could not be opened [#8991](https://github.com/JabRef/jabref/issues/8991)
- We fixed an issue where linked fails containing parts of the main file directory could not be opened. [#8991](https://github.com/JabRef/jabref/issues/8991)
- Linked files with an absolute path can be opened again. [#8991](https://github.com/JabRef/jabref/issues/8991)
- We fixed an issue where the user could not rate an entry in the main table when an entry was not yet ranked. [#5842](https://github.com/JabRef/jabref/issues/5842)
- We fixed an issue that caused JabRef to sometimes open multiple instances when "Remote Operation" is enabled. [#8653](https://github.com/JabRef/jabref/issues/8653)
- We fixed an issue where linked files with the filetype "application/pdf" in an entry were not shown with the correct PDF-Icon in the main table [8930](https://github.com/JabRef/jabref/issues/8930)
Expand Down
76 changes: 63 additions & 13 deletions src/main/java/org/jabref/logic/importer/util/FileFieldParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,36 @@
public class FileFieldParser {
private static final Logger LOGGER = LoggerFactory.getLogger(FileFieldParser.class);

private final String value;

private StringBuilder charactersOfCurrentElement;
private boolean windowsPath;

public FileFieldParser(String value) {
this.value = value;
}

/**
* Converts the string representation of LinkedFileData to a List of LinkedFile
*
* The syntax of one element is description:path:type
* Multiple elements are concatenated with ;
*
* The main challenges of the implementation are:
*
* <ul>
* <li>that XML characters might be included (thus one cannot simply split on ";")</li>
* <li>some characters might be escaped</li>
* <li>Windows absolute paths might be included without escaping</li>
* </ul>
*/
public static List<LinkedFile> parse(String value) {
// We need state to have a more clean code. Thus, we instantiate the class and then return the result
FileFieldParser fileFieldParser = new FileFieldParser(value);
return fileFieldParser.parse();
}

public List<LinkedFile> parse() {
List<LinkedFile> files = new ArrayList<>();

if ((value == null) || value.trim().isEmpty()) {
Expand All @@ -32,51 +61,72 @@ public static List<LinkedFile> parse(String value) {
}
}

// data of each LinkedFile as split string
List<String> linkedFileData = new ArrayList<>();
StringBuilder sb = new StringBuilder();

resetDataStructuresForNextElement();
boolean inXmlChar = false;
boolean escaped = false;

for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (!escaped && (c == '\\')) {
escaped = true;
continue;
if (windowsPath) {
charactersOfCurrentElement.append(c);
continue;
} else {
escaped = true;
continue;
}
} else if (!escaped && (c == '&') && !inXmlChar) {
// Check if we are entering an XML special character construct such
// as "&#44;", because we need to know in order to ignore the semicolon.
sb.append(c);
charactersOfCurrentElement.append(c);
if ((value.length() > (i + 1)) && (value.charAt(i + 1) == '#')) {
inXmlChar = true;
}
} else if (!escaped && inXmlChar && (c == ';')) {
// Check if we are exiting an XML special character construct:
sb.append(c);
charactersOfCurrentElement.append(c);
inXmlChar = false;
} else if (!escaped && (c == ':')) {
// We are in the next LinkedFile data element
linkedFileData.add(sb.toString());
sb = new StringBuilder();
if ((linkedFileData.size() == 1) && // we already parsed the description
(charactersOfCurrentElement.length() == 1)) { // we parsed one character
// special case of Windows paths
// Example: ":c:\test.pdf:PDF"
// We are at the second : (position 3 in the example) and "just" add it to the current element
charactersOfCurrentElement.append(c);
windowsPath = true;
} else {
// We are in the next LinkedFile data element
linkedFileData.add(charactersOfCurrentElement.toString());
resetDataStructuresForNextElement();
}
} else if (!escaped && (c == ';') && !inXmlChar) {
linkedFileData.add(sb.toString());
linkedFileData.add(charactersOfCurrentElement.toString());
files.add(convert(linkedFileData));

// next iteration
sb = new StringBuilder();
resetDataStructuresForNextElement();
} else {
sb.append(c);
charactersOfCurrentElement.append(c);
}
escaped = false;
}
if (sb.length() > 0) {
linkedFileData.add(sb.toString());
if (charactersOfCurrentElement.length() > 0) {
linkedFileData.add(charactersOfCurrentElement.toString());
}
if (!linkedFileData.isEmpty()) {
files.add(convert(linkedFileData));
}
return files;
}

private void resetDataStructuresForNextElement() {
charactersOfCurrentElement = new StringBuilder();
windowsPath = false;
}

/**
* Converts the given textual representation of a LinkedFile object
*
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/jabref/logic/util/io/FileNameCleaner.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* This class is based on http://stackoverflow.com/a/5626340/873282
* extended with LEFT CURLY BRACE and RIGHT CURLY BRACE
* Replaces illegal characters in given file paths.
*
* Regarding the maximum length, see {@link FileUtil#getValidFileName(String)}
*/
public class FileNameCleaner {

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/jabref/logic/util/io/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public static String getBaseName(Path fileNameWithExtension) {
* Returns a valid filename for most operating systems.
* <p>
* Currently, only the length is restricted to 255 chars, see MAXIMUM_FILE_NAME_LENGTH.
*
* See also {@link FileHelper#detectBadFileName(String)} and {@link FileNameCleaner#cleanFileName(String)}
*/
public static String getValidFileName(String fileName) {
String nameWithoutExtension = getBaseName(fileName);
Expand Down
6 changes: 0 additions & 6 deletions src/main/java/org/jabref/model/entry/LinkedFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,6 @@ public boolean equals(Object o) {

/**
* Writes serialized object to ObjectOutputStream, automatically called
*
* @param out {@link ObjectOutputStream}
* @throws IOException
*/
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeUTF(getFileType());
Expand All @@ -131,9 +128,6 @@ private void writeObject(ObjectOutputStream out) throws IOException {

/**
* Reads serialized object from ObjectInputStreamm, automatically called
*
* @param in {@link ObjectInputStream}
* @throws IOException
*/
private void readObject(ObjectInputStream in) throws IOException {
fileType = new SimpleStringProperty(in.readUTF());
Expand Down
23 changes: 19 additions & 4 deletions src/main/java/org/jabref/model/util/FileHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -42,7 +43,7 @@ public class FileHelper {
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 34,
42,
58,
58, // ":"
60, 62, 63,
123, 124, 125
};
Expand Down Expand Up @@ -133,10 +134,22 @@ public static Optional<Path> find(String fileName, List<Path> directories) {
/**
* Detect illegal characters in given filename.
*
* See also {@link org.jabref.logic.util.io.FileNameCleaner#cleanFileName}
*
* @param fileName the fileName to detect
* @return Boolean whether there is a illegal name.
* @return Boolean whether there is an illegal name.
*/
public static boolean detectBadFileName(String fileName) {
// fileName could be a path, we want to check the fileName only (and don't care about the path)
// Reason: Handling of "c:\temp.pdf" is difficult, because ":" is an illegal character in the file name,
// but a perfectly legal one in the path at this position
try {
fileName = Path.of(fileName).getFileName().toString();
} catch (InvalidPathException e) {
// in case the internal method cannot parse the path, it is surely illegal
return true;
}

for (int i = 0; i < fileName.length(); i++) {
char c = fileName.charAt(i);
if (!isCharLegal(c)) {
Expand All @@ -161,12 +174,14 @@ public static boolean isCharLegal(char c) {
public static Optional<Path> find(String fileName, Path directory) {
Objects.requireNonNull(fileName);
Objects.requireNonNull(directory);
// Explicitly check for an empty String, as File.exists returns true on that empty path, because it maps to the default jar location
// if we then call toAbsoluteDir, it would always return the jar-location folder. This is not what we want here

if (detectBadFileName(fileName)) {
LOGGER.error("Invalid characters in path for file {} ", fileName);
return Optional.empty();
}

// Explicitly check for an empty string, as File.exists returns true on that empty path, because it maps to the default jar location.
// If we then call toAbsoluteDir, it would always return the jar-location folder. This is not what we want here.
if (fileName.isEmpty()) {
return Optional.of(directory);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static void doNotUseApacheCommonsLang3(JavaClasses classes) {

@ArchTest
public static void doNotUseSwing(JavaClasses classes) {
// This checks for all all Swing packages, but not the UndoManager
// This checks for all Swing packages, but not the UndoManager
noClasses().that().areNotAnnotatedWith(AllowedToUseSwing.class)
.should().accessClassesThat()
.resideInAnyPackage("javax.swing",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void check(LinkedFile expected, List<String> input) {
assertEquals(expected, FileFieldParser.convert(new ArrayList<>(input)));
}

private static Stream<Arguments> stringsToParseTestData() throws Exception {
private static Stream<Arguments> stringsToParseTest() throws Exception {
return Stream.of(
// null string
Arguments.of(
Expand Down Expand Up @@ -114,6 +114,18 @@ private static Stream<Arguments> stringsToParseTestData() throws Exception {
"desc:C\\:\\\\test.pdf:PDF"
),

// handleNonEscapedFilePath
Arguments.of(
Collections.singletonList(new LinkedFile("desc", Path.of("C:\\test.pdf"), "PDF")),
"desc:C:\\test.pdf:PDF"
),

// Source: https://github.com/JabRef/jabref/issues/8991#issuecomment-1214131042
Arguments.of(
Collections.singletonList(new LinkedFile("Boyd2012.pdf", Path.of("C:\\Users\\Literature_database\\Boyd2012.pdf"), "PDF")),
"Boyd2012.pdf:C\\:\\\\Users\\\\Literature_database\\\\Boyd2012.pdf:PDF"
),

// subsetOfFieldsResultsInFileLink: description only
Arguments.of(
Collections.singletonList(new LinkedFile("", Path.of("file.pdf"), "")),
Expand Down Expand Up @@ -170,8 +182,8 @@ private static Stream<Arguments> stringsToParseTestData() throws Exception {
}

@ParameterizedTest
@MethodSource("stringsToParseTestData")
public void testParse(List<LinkedFile> expected, String input) {
@MethodSource
public void stringsToParseTest(List<LinkedFile> expected, String input) {
assertEquals(expected, FileFieldParser.parse(input));
}
}
27 changes: 27 additions & 0 deletions src/test/java/org/jabref/model/util/FileHelperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
import java.nio.file.Path;
import java.util.Optional;

import org.jabref.architecture.AllowedToUseLogic;
import org.jabref.logic.util.OS;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@AllowedToUseLogic("Needs access to OS, because of OS-specific tests")
class FileHelperTest {

@Test
Expand Down Expand Up @@ -65,4 +71,25 @@ public void testDoesNotFindsFileStartingWithTheSameDirectoryHasASubdirectory(@Te
Files.createFile(testFile);
assertEquals(Optional.of(testFile.toAbsolutePath()), FileHelper.find("files/test.pdf", firstFilesPath));
}

public void testCTemp() {
String fileName = "c:\\temp.pdf";
if (OS.WINDOWS) {
assertFalse(FileHelper.detectBadFileName(fileName));
} else {
assertTrue(FileHelper.detectBadFileName(fileName));
}
}

@ParameterizedTest
@ValueSource(strings = {"/mnt/tmp/test.pdf"})
public void legalPaths(String fileName) {
assertFalse(FileHelper.detectBadFileName(fileName));
}

@ParameterizedTest
@ValueSource(strings = {"te{}mp.pdf"})
public void illegalPaths(String fileName) {
assertTrue(FileHelper.detectBadFileName(fileName));
}
}