diff --git a/src/etc/testcases/taskdefs/copy.xml b/src/etc/testcases/taskdefs/copy.xml index bf4441c17..2601556fe 100644 --- a/src/etc/testcases/taskdefs/copy.xml +++ b/src/etc/testcases/taskdefs/copy.xml @@ -265,4 +265,31 @@ a=b= + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/org/apache/tools/ant/util/ResourceUtils.java b/src/main/org/apache/tools/ant/util/ResourceUtils.java index 9b246a0a3..c8989f9d0 100644 --- a/src/main/org/apache/tools/ant/util/ResourceUtils.java +++ b/src/main/org/apache/tools/ant/util/ResourceUtils.java @@ -417,7 +417,7 @@ public class ResourceUtils { final File sourceFile = source.as(FileProvider.class).getFile(); try { - copyUsingFileChannels(sourceFile, destFile); + copyUsingFileChannels(sourceFile, destFile, project); copied = true; } catch (final IOException ex) { String msg = "Attempt to copy " + sourceFile @@ -641,11 +641,17 @@ public class ResourceUtils { final Project project) throws IOException { + if (areSame(source, dest)) { + // copying the "same" file to itself will corrupt the file, so we skip it + log(project, "Skipping (self) copy of " + source + " to " + dest); + return; + } + try (Reader in = filterWith(project, inputEncoding, filterChains, - source.getInputStream()); - BufferedWriter out = new BufferedWriter(new OutputStreamWriter( - getOutputStream(dest, append, project), - charsetFor(outputEncoding)))) { + source.getInputStream()); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter( + getOutputStream(dest, append, project), + charsetFor(outputEncoding)))) { final LineTokenizer lineTokenizer = new LineTokenizer(); lineTokenizer.setIncludeDelims(true); @@ -691,11 +697,18 @@ public class ResourceUtils { final String outputEncoding, final Project project) throws IOException { + + if (areSame(source, dest)) { + // copying the "same" file to itself will corrupt the file, so we skip it + log(project, "Skipping (self) copy of " + source + " to " + dest); + return; + } + try (Reader in = filterWith(project, inputEncoding, filterChains, - source.getInputStream()); - BufferedWriter out = new BufferedWriter(new OutputStreamWriter( - getOutputStream(dest, append, project), - charsetFor(outputEncoding)))) { + source.getInputStream()); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter( + getOutputStream(dest, append, project), + charsetFor(outputEncoding)))) { final char[] buffer = new char[FileUtils.BUF_SIZE]; while (true) { final int nRead = in.read(buffer, 0, buffer.length); @@ -705,12 +718,18 @@ public class ResourceUtils { out.write(buffer, 0, nRead); } } + } private static void copyUsingFileChannels(final File sourceFile, - final File destFile) + final File destFile, final Project project) throws IOException { + if (FileUtils.getFileUtils().areSame(sourceFile, destFile)) { + // copying the "same" file to itself will corrupt the file, so we skip it + log(project, "Skipping (self) copy of " + sourceFile + " to " + destFile); + return; + } final File parent = destFile.getParentFile(); if (parent != null && !parent.isDirectory() && !(parent.mkdirs() || parent.isDirectory())) { @@ -738,8 +757,14 @@ public class ResourceUtils { private static void copyUsingStreams(final Resource source, final Resource dest, final boolean append, final Project project) throws IOException { + + if (areSame(source, dest)) { + // copying the "same" file to itself will corrupt the file, so we skip it + log(project, "Skipping (self) copy of " + source + " to " + dest); + return; + } try (InputStream in = source.getInputStream(); - OutputStream out = getOutputStream(dest, append, project)) { + OutputStream out = getOutputStream(dest, append, project)) { final byte[] buffer = new byte[FileUtils.BUF_SIZE]; int count = 0; @@ -768,6 +793,33 @@ public class ResourceUtils { return resource.getOutputStream(); } + private static boolean areSame(final Resource resource1, final Resource resource2) throws IOException { + if (resource1 == null || resource2 == null) { + return false; + } + final FileProvider fileResource1 = resource1.as(FileProvider.class); + if (fileResource1 == null) { + return false; + } + final FileProvider fileResource2 = resource2.as(FileProvider.class); + if (fileResource2 == null) { + return false; + } + return FileUtils.getFileUtils().areSame(fileResource1.getFile(), fileResource2.getFile()); + } + + private static void log(final Project project, final String message) { + log(project, message, Project.MSG_VERBOSE); + } + + private static void log(final Project project, final String message, final int level) { + if (project == null) { + System.out.println(message); + } else { + project.log(message, level); + } + } + public interface ResourceSelectorProvider { ResourceSelector getTargetSelectorForSource(Resource source); } diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java index a7a32a9a5..719bc4456 100644 --- a/src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java @@ -21,12 +21,16 @@ package org.apache.tools.ant.taskdefs; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildFileRule; import org.apache.tools.ant.FileUtilities; +import org.apache.tools.ant.taskdefs.condition.Os; import org.apache.tools.ant.util.FileUtils; +import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; @@ -189,7 +193,7 @@ public class CopyTest { assertTrue(ex.getMessage().endsWith(" does not exist.")); } } - + @Test public void testFileResourcePlain() { buildRule.executeTarget("testFileResourcePlain"); @@ -212,7 +216,7 @@ public class CopyTest { assertTrue(file2.exists()); assertTrue(file3.exists()); } - + @Test public void testFileResourceWithFilter() { buildRule.executeTarget("testFileResourceWithFilter"); @@ -225,7 +229,7 @@ public class CopyTest { // no-op: not a real business error } } - + @Test public void testPathAsResource() { buildRule.executeTarget("testPathAsResource"); @@ -236,7 +240,7 @@ public class CopyTest { assertTrue(file2.exists()); assertTrue(file3.exists()); } - + @Test public void testZipfileset() { buildRule.executeTarget("testZipfileset"); @@ -252,7 +256,7 @@ public class CopyTest { public void testDirset() { buildRule.executeTarget("testDirset"); } - + @Ignore("Previously ignored due to naming convention") @Test public void testResourcePlain() { @@ -276,5 +280,60 @@ public class CopyTest { public void testOnlineResources() { buildRule.executeTarget("testOnlineResources"); } - + + /** + * Tests that the {@code copy} task doesn't corrupt the source file, if the target of the copy operation is a symlink + * to the source file being copied + * + * @throws Exception + * @see issue 60644 + */ + @Test + public void testCopyToSymlinkedSelf() throws Exception { + // we are only going to test against systems that support symlinks + Assume.assumeTrue("Symlinks not supported on this operating system", Os.isFamily(Os.FAMILY_UNIX)); + + // setup the source files to run copying against + buildRule.executeTarget("setupSelfCopyTesting"); + final File testDir = new File(buildRule.getProject().getProperty("self.copy.test.root.dir")); + Assert.assertTrue(testDir + " was expected to be a directory", testDir.isDirectory()); + final File srcFile = new File(testDir, "file.txt"); + Assert.assertTrue("Source file " + srcFile + " was expected to be a file", srcFile.isFile()); + final long originalFileSize = srcFile.length(); + final String originalContent; + final BufferedReader reader = new BufferedReader(new FileReader(srcFile)); + try { + originalContent = FileUtils.readFully(reader); + } finally { + reader.close(); + } + Assert.assertTrue("Content missing in file " + srcFile, originalContent != null && originalContent.length() > 0); + + // run the copy tests + buildRule.executeTarget("testSelfCopy"); + // make sure the source file hasn't been impacted by the copy + assertSizeAndContent(srcFile, originalFileSize, originalContent); + final File symlinkedFile = new File(testDir, "sylmink-file.txt"); + Assert.assertTrue(symlinkedFile + " was expected to be a file", symlinkedFile.isFile()); + assertSizeAndContent(symlinkedFile, originalFileSize, originalContent); + + final File symlinkedTestDir = new File(buildRule.getProject().getProperty("self.copy.test.symlinked.dir")); + Assert.assertTrue(symlinkedTestDir + " was expected to be a directory", symlinkedTestDir.isDirectory()); + assertSizeAndContent(new File(symlinkedTestDir, "file.txt"), originalFileSize, originalContent); + assertSizeAndContent(new File(symlinkedTestDir, "sylmink-file.txt"), originalFileSize, originalContent); + + } + + private void assertSizeAndContent(final File file, final long expectedSize, final String expectedContent) throws IOException { + Assert.assertTrue(file + " was expected to be a file", file.isFile()); + Assert.assertEquals("Unexpected size of file " + file, expectedSize, file.length()); + final BufferedReader reader = new BufferedReader(new FileReader(file)); + final String content; + try { + content = FileUtils.readFully(reader); + } finally { + reader.close(); + } + Assert.assertEquals("Unexpected content in file " + file, expectedContent, content); + } }