Skip to content
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
63 changes: 41 additions & 22 deletions src/main/java/org/codehaus/plexus/util/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1176,45 +1177,53 @@
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 {
if (file.isDirectory()) {
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);
}
}

/**
Expand Down Expand Up @@ -1247,7 +1256,9 @@
}
directory.deleteOnExit(); // The hook reverses the list

cleanDirectoryOnExit(directory);
if (!NioFiles.isSymbolicLink(directory)) {
cleanDirectoryOnExit(directory);
}
}

/**
Expand All @@ -1270,6 +1281,9 @@
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);
Expand Down Expand Up @@ -1343,7 +1357,9 @@
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);
Expand Down Expand Up @@ -1762,6 +1778,9 @@
}

File[] files = sourceDirectory.listFiles();
if (files == null) {
throw new IOException("Could not list directory " + sourceDirectory.getAbsolutePath());
}

String sourcePath = sourceDirectory.getAbsolutePath();

Expand Down Expand Up @@ -1922,9 +1941,9 @@
fileReader = null;
} finally {
//noinspection deprecation
IOUtil.close(fileReader);

Check warning on line 1944 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-26.04 jdk-25-zulu 3.10.0

close(java.io.Reader) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1944 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-25-zulu 3.10.0

close(java.io.Reader) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1944 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-8-zulu 3.10.0

close(java.io.Reader) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1944 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / macos-latest jdk-25-zulu 3.10.0

close(java.io.Reader) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1944 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-21-zulu 3.10.0

close(java.io.Reader) in org.codehaus.plexus.util.IOUtil has been deprecated
//noinspection deprecation
IOUtil.close(fileWriter);

Check warning on line 1946 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-26.04 jdk-25-zulu 3.10.0

close(java.io.Writer) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1946 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-25-zulu 3.10.0

close(java.io.Writer) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1946 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-8-zulu 3.10.0

close(java.io.Writer) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1946 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / macos-latest jdk-25-zulu 3.10.0

close(java.io.Writer) in org.codehaus.plexus.util.IOUtil has been deprecated

Check warning on line 1946 in src/main/java/org/codehaus/plexus/util/FileUtils.java

View workflow job for this annotation

GitHub Actions / Build it / Verify / ubuntu-latest jdk-21-zulu 3.10.0

close(java.io.Writer) in org.codehaus.plexus.util.IOUtil has been deprecated
}
} else {
if (isSourceNewerThanDestination(from, to) || overwrite) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> v = new Vector<String>();
StringBuilder current = new StringBuilder();

Expand All @@ -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);
Expand Down
101 changes: 101 additions & 0 deletions src/test/java/org/codehaus/plexus/util/FileUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading