From 06f4f6650f10423a5f76e28e4ccb57bbfd906ec4 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 8 Nov 2026 11:15:40 +0000 Subject: [PATCH 1/5] Report an unlistable directory as IOException in copyDirectoryStructure and cleanDirectoryOnExit File.listFiles() returns null when a directory cannot be read; both methods dereferenced it and failed with a NullPointerException that named nothing. cleanDirectory already checks for null. (cherry picked from commit 30d3090fcfcd5b4dffbd66a37cd9ff68afdacd55) --- .../java/org/codehaus/plexus/util/FileUtils.java | 6 ++++++ .../org/codehaus/plexus/util/FileUtilsTest.java | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index b411272a..f14771c8 100644 --- a/src/main/java/org/codehaus/plexus/util/FileUtils.java +++ b/src/main/java/org/codehaus/plexus/util/FileUtils.java @@ -1270,6 +1270,9 @@ private static void cleanDirectoryOnExit(final File directory) throws IOExceptio IOException exception = null; final File[] files = directory.listFiles(); + if (files == null) { + throw new IOException("Could not list directory " + directory.getAbsolutePath()); + } for (final File file : files) { try { forceDeleteOnExit(file); @@ -1762,6 +1765,9 @@ private static void copyDirectoryStructure( } File[] files = sourceDirectory.listFiles(); + if (files == null) { + throw new IOException("Could not list directory " + sourceDirectory.getAbsolutePath()); + } String sourcePath = sourceDirectory.getAbsolutePath(); diff --git a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 7474a777..65de89b5 100644 --- a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.condition.OS; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * This is used to test FileUtils for correctness. @@ -687,6 +688,21 @@ void removeExtensionWithPaths() { } } + @Test + void copyDirectoryStructureFailsWhenSourceCannotBeListed() throws Exception { + File from = new File(getTestDirectory(), "unlistable"); + from.mkdirs(); + File to = new File(getTestDirectory(), "unlistable-copy"); + assumeTrue(from.setReadable(false) && from.setExecutable(false) && from.listFiles() == null); + try { + IOException e = assertThrows(IOException.class, () -> FileUtils.copyDirectoryStructure(from, to)); + assertTrue(e.getMessage().contains(from.getAbsolutePath())); + } finally { + from.setExecutable(true); + from.setReadable(true); + } + } + @Test void copyDirectoryStructureWithAEmptyDirectoryStructure() throws Exception { File from = new File(getTestDirectory(), "from"); From dc172c741c8fce4e3de13643b118c88091831a2e Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 8 Nov 2026 21:05:10 +0100 Subject: [PATCH 2/5] Use Files.deleteIfExists in FileUtils.forceDelete Retry with System.gc() and a sleep only for files that still exist after a failed delete, so deleting an absent file no longer pays that cost on Windows. Files.deleteIfExists also removes dangling symlinks. (cherry picked from commit 2eda606034159a6b21668797e93e7dd1963081f0) --- .../org/codehaus/plexus/util/FileUtils.java | 49 +++++++++++-------- .../codehaus/plexus/util/FileUtilsTest.java | 20 ++++++++ 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index f14771c8..a09fa1a1 100644 --- a/src/main/java/org/codehaus/plexus/util/FileUtils.java +++ b/src/main/java/org/codehaus/plexus/util/FileUtils.java @@ -66,6 +66,7 @@ import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; @@ -1176,23 +1177,20 @@ public static void forceDelete(final File file) throws IOException { if (file.isDirectory()) { deleteDirectory(file); } else { - /* - * NOTE: Always try to delete the file even if it appears to be non-existent. This will ensure that a - * symlink whose target does not exist is deleted, too. - */ - boolean filePresent = file.getCanonicalFile().exists(); - if (!deleteFile(file) && filePresent) { - final String message = "File " + file + " unable to be deleted."; - throw new IOException(message); + if (!deleteFile(file)) { + throw new IOException("File " + file + " unable to be deleted."); } } } /** - * Accommodate Windows bug encountered in both Sun and IBM JDKs. Others possible. If the delete does not work, call + * Deletes a file, or a symlink without following it. A file that does not exist (including a dangling symlink, + * which {@link Files#deleteIfExists} removes) counts as deleted and returns immediately. Only if the file still + * exists after a failed attempt, accommodate the Windows bug encountered in both Sun and IBM JDKs: call * System.gc(), wait a little and try again. * * @param file a file + * @return {@code true} if the file no longer exists * @throws IOException if any */ private static boolean deleteFile(File file) throws IOException { @@ -1200,21 +1198,32 @@ private static boolean deleteFile(File file) throws IOException { throw new IOException("File " + file + " isn't a file."); } - if (!file.delete()) { - if (Os.isFamily(Os.FAMILY_WINDOWS)) { - file = file.getCanonicalFile(); - System.gc(); + final Path path = file.toPath(); + try { + Files.deleteIfExists(path); + return true; + } catch (IOException e) { + if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { + return true; } + } - try { - Thread.sleep(10); - return file.delete(); - } catch (InterruptedException ignore) { - return file.delete(); - } + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + System.gc(); } - return true; + try { + Thread.sleep(10); + } catch (InterruptedException ignore) { + Thread.currentThread().interrupt(); + } + + try { + Files.deleteIfExists(path); + return true; + } catch (IOException e) { + return !Files.exists(path, LinkOption.NOFOLLOW_LINKS); + } } /** diff --git a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 65de89b5..32d4e0d7 100644 --- a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java @@ -433,6 +433,26 @@ void forceDeleteAFile2() throws Exception { assertFalse(destination.exists(), "Check No Exist"); } + @Test + void forceDeleteNonExistingFile() throws Exception { + final File missing = new File(getTestDirectory(), "does-not-exist.txt"); + assertFalse(missing.exists()); + FileUtils.forceDelete(missing); + assertFalse(missing.exists()); + } + + @Test + void forceDeleteDanglingSymlink() throws Exception { + final File target = new File(getTestDirectory(), "dangling-target.txt"); + final File symlink = new File(getTestDirectory(), "dangling-link"); + createFile(target, 1); + createSymlink(symlink, target); + assertTrue(target.delete()); + assertTrue(Files.isSymbolicLink(symlink.toPath()), "Link should be left dangling"); + FileUtils.forceDelete(symlink); + assertFalse(Files.isSymbolicLink(symlink.toPath()), "Dangling symlink should be removed"); + } + @Test void copyFile1ToDir() throws Exception { final File directory = new File(getTestDirectory(), "subdir"); From 67c89ecd63136724cc91c007ebd6502824a76e4c Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 9 Nov 2026 00:40:46 +0100 Subject: [PATCH 3/5] Skip the dangling-symlink test where the platform cannot keep one Windows stops reporting a file link as a symlink once its target is deleted, so the precondition of the test, not the fix, failed there. (cherry picked from commit 44a05db2d5b33cef58a24eb6a82374cbb32dcf4c) --- src/test/java/org/codehaus/plexus/util/FileUtilsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 32d4e0d7..284ea7fa 100644 --- a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java @@ -447,8 +447,10 @@ void forceDeleteDanglingSymlink() throws Exception { final File symlink = new File(getTestDirectory(), "dangling-link"); createFile(target, 1); createSymlink(symlink, target); + assumeTrue(Files.isSymbolicLink(symlink.toPath()), "the platform created a real symlink"); assertTrue(target.delete()); - assertTrue(Files.isSymbolicLink(symlink.toPath()), "Link should be left dangling"); + // Windows stops reporting a file link as a symlink once its target is gone + assumeTrue(Files.isSymbolicLink(symlink.toPath()), "the platform keeps a dangling symlink"); FileUtils.forceDelete(symlink); assertFalse(Files.isSymbolicLink(symlink.toPath()), "Dangling symlink should be removed"); } From 3f513333fe8c89ca2d727fae3d4ea373c4a88824 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 8 Nov 2026 21:28:59 +0100 Subject: [PATCH 4/5] Split CommandLineUtils.translateCommandline at tabs and line breaks (cherry picked from commit ba9039237f12c089ea723a59b7ecce6aa637ba9e) --- .../plexus/util/cli/CommandLineUtils.java | 7 +++++-- .../plexus/util/cli/CommandLineUtilsTest.java | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/cli/CommandLineUtils.java b/src/main/java/org/codehaus/plexus/util/cli/CommandLineUtils.java index b2ac489e..a7bdb01a 100644 --- a/src/main/java/org/codehaus/plexus/util/cli/CommandLineUtils.java +++ b/src/main/java/org/codehaus/plexus/util/cli/CommandLineUtils.java @@ -356,7 +356,7 @@ public static String[] translateCommandline(String toProcess) throws Exception { final int inQuote = 1; final int inDoubleQuote = 2; int state = normal; - StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); + StringTokenizer tok = new StringTokenizer(toProcess, "\"\' \t\n\r", true); Vector v = new Vector(); StringBuilder current = new StringBuilder(); @@ -382,7 +382,10 @@ public static String[] translateCommandline(String toProcess) throws Exception { state = inQuote; } else if ("\"".equals(nextTok)) { state = inDoubleQuote; - } else if (" ".equals(nextTok)) { + } else if (" ".equals(nextTok) + || "\t".equals(nextTok) + || "\n".equals(nextTok) + || "\r".equals(nextTok)) { if (current.length() != 0) { v.addElement(current.toString()); current.setLength(0); diff --git a/src/test/java/org/codehaus/plexus/util/cli/CommandLineUtilsTest.java b/src/test/java/org/codehaus/plexus/util/cli/CommandLineUtilsTest.java index f9852c37..f19cf5e2 100644 --- a/src/test/java/org/codehaus/plexus/util/cli/CommandLineUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/cli/CommandLineUtilsTest.java @@ -98,6 +98,23 @@ void translateCommandline() throws Exception { assertCmdLineArgs(new String[] {"foo", " ' ", "bar"}, "foo \" ' \" bar"); } + @Test + void translateCommandlineSplitsAtTabsAndLineBreaks() throws Exception { + assertCmdLineArgs(new String[] {"foo", "bar"}, "foo\tbar"); + assertCmdLineArgs(new String[] {"foo", "bar"}, "foo\nbar"); + assertCmdLineArgs(new String[] {"foo", "bar"}, "foo\rbar"); + assertCmdLineArgs(new String[] {"foo", "bar"}, "foo\r\nbar"); + assertCmdLineArgs(new String[] {"foo", "bar", "baz"}, " \t foo \t\n bar\r\n\tbaz \n"); + assertCmdLineArgs(new String[] {}, " \t\r\n "); + } + + @Test + void translateCommandlineKeepsWhitespaceInsideQuotes() throws Exception { + assertCmdLineArgs(new String[] {"foo", "a\tb\nc\r", "bar"}, "foo \"a\tb\nc\r\" bar"); + assertCmdLineArgs(new String[] {"foo", "a\tb\nc\r", "bar"}, "foo 'a\tb\nc\r' bar"); + assertCmdLineArgs(new String[] {"a\tb", "c"}, "'a\tb'\tc"); + } + private void assertCmdLineArgs(String[] expected, String cmdLine) throws Exception { String[] actual = CommandLineUtils.translateCommandline(cmdLine); assertNotNull(actual); From 6e954c244052a934383e92548ce243b1f996809c Mon Sep 17 00:00:00 2001 From: neha khan Date: Mon, 2 Nov 2026 17:05:17 +0530 Subject: [PATCH 5/5] Do not follow symlinks in forceDeleteOnExit and deleteDirectory forceDeleteOnExit recursed through File.isDirectory(), which follows symbolic links, so a link to a directory outside the tree got every file under its target scheduled for deletion on exit. deleteDirectory did the same on its fallback path when the link itself could not be removed. Skip the recursion for symbolic links in both places. (cherry picked from commit bd9636767dbfac53956c53f816914fa149431aee) --- .../org/codehaus/plexus/util/FileUtils.java | 8 ++- .../codehaus/plexus/util/FileUtilsTest.java | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index a09fa1a1..9016d7f4 100644 --- a/src/main/java/org/codehaus/plexus/util/FileUtils.java +++ b/src/main/java/org/codehaus/plexus/util/FileUtils.java @@ -1256,7 +1256,9 @@ private static void deleteDirectoryOnExit(final File directory) throws IOExcepti } directory.deleteOnExit(); // The hook reverses the list - cleanDirectoryOnExit(directory); + if (!NioFiles.isSymbolicLink(directory)) { + cleanDirectoryOnExit(directory); + } } /** @@ -1355,7 +1357,9 @@ public static void deleteDirectory(final File directory) throws IOException { return; } - cleanDirectory(directory); + if (!NioFiles.isSymbolicLink(directory)) { + cleanDirectory(directory); + } if (!directory.delete()) { final String message = "Directory " + directory + " unable to be deleted."; throw new IOException(message); diff --git a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 284ea7fa..a8fa8c2f 100644 --- a/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java +++ b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java @@ -31,14 +31,17 @@ import java.nio.file.Paths; import java.util.Optional; import java.util.Properties; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; /** @@ -1231,6 +1234,66 @@ void deleteDirectoryWithDanglingSymlink() throws Exception { assertFalse(getTestDirectory().exists(), "Failed to delete test directory"); } + @Test + @DisabledOnOs(OS.WINDOWS) + void deleteDirectoryDoesNotFollowDirSymlink() throws Exception { + File outside = new File(getTestDirectory(), "outside"); + outside.mkdir(); + File outsideFile = new File(outside, "keep.txt"); + createFile(outsideFile, 1); + + File directory = new File(getTestDirectory(), "directory"); + File readOnly = new File(directory, "readOnly"); + readOnly.mkdirs(); + File symlink = new File(readOnly, "dirSymlink"); + createSymlink(symlink, outside); + // the symlink cannot be unlinked from a read-only directory + readOnly.setWritable(false); + try { + assumeFalse(Files.isWritable(readOnly.toPath()), "directory permissions are not enforced"); + assertThrows(IOException.class, () -> FileUtils.deleteDirectory(directory)); + } finally { + readOnly.setWritable(true); + symlink.delete(); + } + assertTrue(outsideFile.exists(), "deleteDirectory followed a symlink out of the directory"); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void forceDeleteOnExitDoesNotFollowDirSymlink() throws Exception { + File outside = new File(getTestDirectory(), "outside"); + outside.mkdir(); + File outsideFile = new File(outside, "keep.txt"); + createFile(outsideFile, 1); + + File directory = new File(getTestDirectory(), "directory"); + directory.mkdir(); + createSymlink(new File(directory, "dirSymlink"), outside); + + // deleteOnExit only runs when the JVM exits, so run it in a separate one + String java = new File(System.getProperty("java.home"), "bin/java").getPath(); + Process process = new ProcessBuilder( + java, + "-cp", + System.getProperty("java.class.path"), + ForceDeleteOnExit.class.getName(), + directory.getAbsolutePath()) + .inheritIO() + .start(); + assertTrue(process.waitFor(1, TimeUnit.MINUTES), "JVM did not exit"); + assertEquals(0, process.exitValue()); + + assertFalse(directory.exists(), "Failed to delete directory on exit"); + assertTrue(outsideFile.exists(), "forceDeleteOnExit followed a symlink out of the directory"); + } + + public static final class ForceDeleteOnExit { + public static void main(String[] args) throws IOException { + FileUtils.forceDeleteOnExit(new File(args[0])); + } + } + @Test void testcopyDirectoryLayoutWithExcludesIncludes() throws Exception { File destination = new File("target", "copyDirectoryStructureWithExcludesIncludes");