Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
133a324
refactor: split OpenExternalFileAction into OpenSingleExternalFileAct…
Muskan244 Jul 8, 2025
91da0cf
Merge branch 'main' into fix-for-issue-13431
Muskan244 Jul 8, 2025
bcf3b76
Cleaned up constant name, dialog text, and removed extra comment
Muskan244 Jul 8, 2025
a8a1235
Merge branch 'main' into fix-for-issue-13431
koppor Jul 8, 2025
6ccf927
Add null checks to OpenSingleExternalFileAction constructor
Muskan244 Jul 8, 2025
4dd6694
Add @NonNull annotations and fix localization key warnings
Muskan244 Jul 8, 2025
90233e4
Merge branch 'main' into fix-for-issue-13431
Muskan244 Jul 8, 2025
dc0d67f
fix submodules
Muskan244 Jul 8, 2025
dd9ca53
Use database latest context, update button text, and add comments exp…
Muskan244 Jul 10, 2025
87b7ca9
Merge branch 'main' into fix-for-issue-13431
Muskan244 Jul 10, 2025
6c654b6
Bump org.junit.platform:junit-platform-launcher in /versions (#13505)
dependabot[bot] Jul 7, 2025
77fd7a1
Fix: clarify fallback logic, update dialog labels, and avoid null Opt…
Muskan244 Jul 11, 2025
e717e07
Fix: Make FileUtil.relativize symlink-aware for robust relative path …
Muskan244 Jul 18, 2025
22522dc
remove unnecessary comments and make test more clearer and maintainable
Muskan244 Jul 19, 2025
9f38281
Merge branch 'main' into fix-for-issue-12995
Muskan244 Jul 19, 2025
8c9257d
Refactor to avoid using exceptions for normal control flow in tryReal…
Muskan244 Jul 19, 2025
bb3b3b2
Merge branch 'fix-for-issue-12995' of https://github.com/Muskan244/ja…
Muskan244 Jul 19, 2025
b394a3c
Merge branch 'main' into fix-for-issue-12995
Muskan244 Jul 20, 2025
1d236aa
Refactored symlink tests for independence, used Files.writeString for…
Muskan244 Jul 20, 2025
5124b5f
Fix code style issues with OpenRewrite and update symlink tests
Muskan244 Jul 20, 2025
ad92c0d
Fix submodules
Muskan244 Jul 20, 2025
cb314a2
Fix submodules
Muskan244 Jul 20, 2025
c60919c
Merge branch 'main' into fix-for-issue-12995
Muskan244 Jul 24, 2025
b7f7587
Fix typo
koppor Aug 4, 2025
4270261
Refactor tests
koppor Aug 4, 2025
18fa797
Add comments to explain the logic to not use directory.relativize()
Muskan244 Aug 17, 2025
e90cafb
Merge branch 'fix-for-issue-12995' of https://github.com/Muskan244/ja…
Muskan244 Aug 17, 2025
9e1952d
Merge branch 'main' into fix-for-issue-12995
Muskan244 Aug 17, 2025
618e12a
Removed unneccesary comment
Muskan244 Aug 17, 2025
3f88a1d
Merge branch 'fix-for-issue-12995' of https://github.com/Muskan244/ja…
Muskan244 Aug 17, 2025
19d7cb3
Add ignored test for symlink escape case (#12995 comment)
Muskan244 Aug 18, 2025
bf94ce8
Format complete file
koppor Aug 19, 2025
77985e1
Add CHANGELOG.md entry
koppor Aug 19, 2025
9814427
Simplify code
koppor Aug 19, 2025
da97e5f
Have the simple case first
koppor Aug 19, 2025
5f12089
Consistent method naming
koppor Aug 19, 2025
824734f
Warn samehow if real path conversion fails
koppor Aug 19, 2025
cf2bc0f
Refine link
koppor Aug 19, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added an initial [cite as you write](https://retorque.re/zotero-better-bibtex/citing/cayw/) endpoint. [#13187](https://github.com/JabRef/jabref/issues/13187)
- We added "copy preview as markdown" feature. [#12552](https://github.com/JabRef/jabref/issues/12552)
- In case no citation relation information can be fetched, we show the data providers reason. [#13549](https://github.com/JabRef/jabref/pull/13549)
- When relativizing file names, symlinks are now taken into account. [#12995](https://github.com/JabRef/jabref/issues/12995)
- We added a new button for shortening the DOI near the DOI field in the general tab when viewing an entry. [#13639](https://github.com/JabRef/jabref/issues/13639)

### Changed
Expand Down
54 changes: 38 additions & 16 deletions jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public static Path addExtension(Path path, String extension) {
/**
* Looks for the unique directory, if any, different to the provided paths
*
* @param paths List of paths as Strings
* @param paths List of paths as Strings
* @param comparePath The to be tested path
*/
public static Optional<String> getUniquePathDirectory(List<String> paths, Path comparePath) {
Expand All @@ -153,15 +153,15 @@ public static Optional<String> getUniquePathDirectory(List<String> paths, Path c
List<String> uniquePathParts = uniquePathSubstrings(paths);
return uniquePathParts.stream()
.filter(part -> comparePath.toString().contains(part)
&& !part.equals(fileName) && part.contains(File.separator))
&& !part.equals(fileName) && part.contains(File.separator))
.findFirst()
.map(part -> part.substring(0, part.lastIndexOf(File.separator)));
}

/**
* Looks for the shortest unique path of the in a list of paths
*
* @param paths List of paths as Strings
* @param paths List of paths as Strings
* @param comparePath The to be shortened path
*/
public static Optional<String> getUniquePathFragment(List<String> paths, Path comparePath) {
Expand Down Expand Up @@ -219,7 +219,7 @@ public static List<String> uniquePathSubstrings(List<String> paths) {
* @param pathToSourceFile Path Source file
* @param pathToDestinationFile Path Destination file
* @param replaceExisting boolean Determines whether the copy goes on even if the file exists.
* @return boolean Whether the copy succeeded, or was stopped due to the file already existing.
* @return boolean Whether the copy succeeded or was stopped due to the file already existing.
*/
public static boolean copyFile(Path pathToSourceFile, Path pathToDestinationFile, boolean replaceExisting) {
// Check if the file already exists.
Expand Down Expand Up @@ -255,15 +255,39 @@ public static Path relativize(Path file, List<Path> directories) {
if (!file.isAbsolute()) {
return file;
}
Optional<Path> realFileOpt = tryRealPath(file);

for (Path directory : directories) {
if (file.startsWith(directory)) {
return directory.relativize(file);
}

if (realFileOpt.isPresent()) {
Optional<Path> realDirOpt = tryRealPath(directory);
if (realDirOpt.isPresent()) {
Path realFile = realFileOpt.get();
Path realDir = realDirOpt.get();
if (realFile.startsWith(realDir)) {
return realDir.relativize(realFile);
}
}
}
}
return file;
}

private static Optional<Path> tryRealPath(Path path) {
if (Files.exists(path)) {
try {
return Optional.of(path.toRealPath());
} catch (IOException e) {
return Optional.empty();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method catches IOException and returns Optional.empty() instead of letting the exception propagate. This uses exceptions for control flow which is against best practices.

} else {
return Optional.of(path.toAbsolutePath());
}
}

/**
* Converts an absolute file to a relative one, if possible. Returns the parameter file itself if no shortening is
* possible.
Expand Down Expand Up @@ -342,8 +366,8 @@ public static String createFileNameFromPattern(BibDatabase database, BibEntry en
/**
* Determines directory name provided by an entry in a database
*
* @param database the database, where the entry is located
* @param entry the entry to which the directory should be linked to
* @param database the database, where the entry is located
* @param entry the entry to which the directory should be linked to
* @param directoryNamePattern the dirname pattern
* @return a suggested dirName
*/
Expand All @@ -370,9 +394,9 @@ public static String createDirNameFromPattern(BibDatabase database, BibEntry ent
public static Optional<Path> findSingleFileRecursively(String filename, Path rootDirectory) {
try (Stream<Path> pathStream = Files.walk(rootDirectory)) {
return pathStream
.filter(Files::isRegularFile)
.filter(f -> f.getFileName().toString().equals(filename))
.findFirst();
.filter(Files::isRegularFile)
.filter(f -> f.getFileName().toString().equals(filename))
.findFirst();
} catch (UncheckedIOException | IOException ex) {
LOGGER.error("Error trying to locate the file {} inside the directory {}", filename, rootDirectory, ex);
}
Expand Down Expand Up @@ -410,9 +434,8 @@ public static Optional<Path> find(String fileName, List<Path> directories) {
/**
* Converts a relative filename to an absolute one, if necessary.
*
* @param fileName the filename (e.g., a .pdf file), may contain path separators
* @param fileName the filename (e.g., a .pdf file), may contain path separators
* @param directory the directory which should be search starting point
*
* @return an empty optional if the file does not exist, otherwise, the absolute path
*/
public static Optional<Path> find(String fileName, Path directory) {
Expand Down Expand Up @@ -504,10 +527,9 @@ public static Path getInitialDirectory(BibDatabaseContext databaseContext, Path
/**
* Detect illegal characters in given filename.
*
* @see org.jabref.logic.util.io.FileNameCleaner#cleanFileName
*
* @param fileName the fileName to detect
* @return Boolean whether there is an illegal name.
* @see org.jabref.logic.util.io.FileNameCleaner#cleanFileName
*/
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)
Expand Down Expand Up @@ -579,9 +601,9 @@ public static String shortenFileName(String fileName, Integer maxLength) {
numCharsAfterEllipsis = Math.min(numCharsAfterEllipsis, name.length() - numCharsBeforeEllipsis);

return name.substring(0, numCharsBeforeEllipsis) +
ELLIPSIS +
name.substring(name.length() - numCharsAfterEllipsis) +
extension;
ELLIPSIS +
name.substring(name.length() - numCharsAfterEllipsis) +
extension;
}

public static boolean isCharLegal(char c) {
Expand Down
92 changes: 79 additions & 13 deletions jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -39,7 +39,7 @@
class FileUtilTest {

@TempDir
static Path bibTempDir;
private static Path bibTempDir;

private final Path nonExistingTestPath = Path.of("nonExistingTestPath");
private Path existingTestFile;
Expand All @@ -54,11 +54,11 @@ void setUpViewModel(@TempDir Path temporaryFolder) throws IOException {

existingTestFile = subDir.resolve("existingTestFile.txt");
Files.createFile(existingTestFile);
Files.write(existingTestFile, "existingTestFile.txt".getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
Files.writeString(existingTestFile, "existingTestFile.txt");

otherExistingTestFile = subDir.resolve("otherExistingTestFile.txt");
Files.createFile(otherExistingTestFile);
Files.write(otherExistingTestFile, "otherExistingTestFile.txt".getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
Files.writeString(otherExistingTestFile, "otherExistingTestFile.txt");
}

@Test
Expand Down Expand Up @@ -222,16 +222,16 @@ void getFileNameWithMultipleDotsString() {
@Test
@DisabledOnOs(value = org.junit.jupiter.api.condition.OS.WINDOWS, disabledReason = "Assumed path separator is /")
void uniquePathSubstrings() {
List<String> paths = List.of("C:/uniquefile.bib",
"C:/downloads/filename.bib",
"C:/mypaper/bib/filename.bib",
"C:/external/mypaper/bib/filename.bib",
"");
List<String> paths = List.of("C:/uniquefile.bib",
"C:/downloads/filename.bib",
"C:/mypaper/bib/filename.bib",
"C:/external/mypaper/bib/filename.bib",
"");
List<String> uniqPath = List.of("uniquefile.bib",
"downloads/filename.bib",
"C:/mypaper/bib/filename.bib",
"external/mypaper/bib/filename.bib",
"");
"downloads/filename.bib",
"C:/mypaper/bib/filename.bib",
"external/mypaper/bib/filename.bib",
"");

List<String> result = FileUtil.uniquePathSubstrings(paths);
assertEquals(uniqPath, result);
Expand Down Expand Up @@ -448,6 +448,72 @@ public void cTemp() {
}
}

@ParameterizedTest
@DisabledOnOs(value = org.junit.jupiter.api.condition.OS.WINDOWS, disabledReason = "Symlink behavior unreliable on windows")
@MethodSource
void relativizeSymlinks(Path file, List<Path> directories, Path expected, String message) {
if (message.startsWith("IGNORED")) {
org.junit.jupiter.api.Assumptions.assumeTrue(false, message);
}
Path result = FileUtil.relativize(file, directories);
assertEquals(expected, result, message);
}

/// Tests for issue <https://github.com/JabRef/jabref/issues/12995>
static Stream<Arguments> relativizeSymlinks() throws IOException {
List<Arguments> result = new ArrayList<>();

Path realDir = bibTempDir.resolve("realDir");
Files.createDirectories(realDir);

// symlinkDir -> realDir
// realDir/simple.pdf
Path simpleFile = Files.createFile(realDir.resolve("simple.pdf"));
Path symlinkDir = bibTempDir.resolve("symlinkDir");
Files.createSymbolicLink(symlinkDir, realDir);
result.add(Arguments.of(simpleFile, List.of(symlinkDir), Path.of("simple.pdf"), "Simple symlink resolves to relative"));

// chainLink1 -> chainLink2 -> chainReal
// chainReal/chained.pdf
Path chainReal = bibTempDir.resolve("chainReal");
Files.createDirectories(chainReal);
Path chainedFile = Files.createFile(chainReal.resolve("chained.pdf"));
Path chainLink2 = bibTempDir.resolve("chainLink2");
Files.createSymbolicLink(chainLink2, chainReal);
Path chainLink1 = bibTempDir.resolve("chainLink1");
Files.createSymbolicLink(chainLink1, chainLink2);
result.add(Arguments.of(chainedFile, List.of(chainLink1), Path.of("chained.pdf"), "Chained symlink resolves to relative"));

// realDir/nestedLink -> realDir/nested
// realDir/nested/nested.pdf
Path nestedDir = realDir.resolve("nested");
Files.createDirectories(nestedDir);
Path nestedFile = Files.createFile(nestedDir.resolve("nested.pdf"));
Path nestedSymlink = realDir.resolve("nestedLink");
Files.createSymbolicLink(nestedSymlink, nestedDir);
result.add(Arguments.of(nestedFile, List.of(nestedSymlink), Path.of("nested.pdf"), "Nested symlink resolves to relative"));

// symlinkDir -> realDir
// outside.pdf
Path outsideFile = Files.createFile(bibTempDir.resolve("outside.pdf"));
result.add(Arguments.of(outsideFile, List.of(symlinkDir), outsideFile, "Unrelated file remains absolute"));

// symlink chain escaping base dir (ignored test case, see #12995 issue comment)
Path veryPrivate = bibTempDir.resolve("veryprivate");
Files.createDirectories(veryPrivate);
Path secretFile = Files.createFile(veryPrivate.resolve("a.pdf"));
Path expensive = bibTempDir.resolve("expensive");
Files.createSymbolicLink(expensive, veryPrivate);
Path things = bibTempDir.resolve("things");
Files.createSymbolicLink(things, expensive);
Path libDir = bibTempDir.resolve("lib");
Files.createDirectories(libDir);
Path bibFile = Files.createFile(libDir.resolve("bib.bib"));
result.add(Arguments.of(secretFile, List.of(things), secretFile, "IGNORED: Symlink chain escaping base dir (#12995 comment)"));

return result.stream();
}

/**
* @implNote Tests inspired by {@link org.jabref.model.database.BibDatabaseContextTest#getFileDirectoriesWithRelativeMetadata}
*/
Expand Down