diff --git a/src/main/java/org/codehaus/plexus/util/FileUtils.java b/src/main/java/org/codehaus/plexus/util/FileUtils.java index b411272a..9016d7f4 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); + } } /** @@ -1247,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); + } } /** @@ -1270,6 +1281,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); @@ -1343,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); @@ -1762,6 +1778,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/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/FileUtilsTest.java b/src/test/java/org/codehaus/plexus/util/FileUtilsTest.java index 7474a777..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,18 @@ 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; /** * This is used to test FileUtils for correctness. @@ -432,6 +436,28 @@ 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); + assumeTrue(Files.isSymbolicLink(symlink.toPath()), "the platform created a real symlink"); + assertTrue(target.delete()); + // 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"); + } + @Test void copyFile1ToDir() throws Exception { final File directory = new File(getTestDirectory(), "subdir"); @@ -687,6 +713,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"); @@ -1193,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"); 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);