| @@ -265,4 +265,31 @@ a=b= | |||||
| </fail> | </fail> | ||||
| </target> | </target> | ||||
| <target name="setupSelfCopyTesting" description="Sets up the necessary files and directories for testSelfCopy task which | |||||
| will be called by the test cases"> | |||||
| <property name="self.copy.test.root.dir" location="${output}/self-copy-test"/> | |||||
| <mkdir dir="${self.copy.test.root.dir}"/> | |||||
| <echo file="${self.copy.test.root.dir}/file.txt" message="hello-world"/> | |||||
| </target> | |||||
| <target name="testSelfCopy"> | |||||
| <property name="self.copy.test.root.dir" location="${output}/self-copy-test"/> | |||||
| <!-- straightforward self-copy --> | |||||
| <copy file="${self.copy.test.root.dir}/file.txt" tofile="${self.copy.test.root.dir}/file.txt" overwrite="true"/> | |||||
| <!-- create a symlink of the source file and then attempt a copy of the original file and the symlink --> | |||||
| <symlink link="${self.copy.test.root.dir}/sylmink-file.txt" resource="${self.copy.test.root.dir}/file.txt"/> | |||||
| <copy file="${self.copy.test.root.dir}/file.txt" tofile="${self.copy.test.root.dir}/sylmink-file.txt" overwrite="true"/> | |||||
| <copy file="${self.copy.test.root.dir}/sylmink-file.txt" tofile="${self.copy.test.root.dir}/file.txt" overwrite="true"/> | |||||
| <!-- create a symlink of the dir containing the source file and then attempt a copy of the original file and the symlink dir --> | |||||
| <property name="self.copy.test.symlinked.dir" location="${output}/symlink-for-output-dir"/> | |||||
| <symlink link="${self.copy.test.symlinked.dir}" resource="${self.copy.test.root.dir}"/> | |||||
| <copy file="${self.copy.test.symlinked.dir}/file.txt" tofile="${self.copy.test.root.dir}/sylmink-file.txt" overwrite="true"/> | |||||
| <copy file="${self.copy.test.root.dir}/sylmink-file.txt" tofile="${self.copy.test.symlinked.dir}/file.txt" overwrite="true"/> | |||||
| <copy todir="${self.copy.test.symlinked.dir}" overwrite="true"> | |||||
| <fileset dir="${self.copy.test.root.dir}"/> | |||||
| </copy> | |||||
| </target> | |||||
| </project> | </project> | ||||
| @@ -432,7 +432,7 @@ public class ResourceUtils { | |||||
| final File sourceFile = | final File sourceFile = | ||||
| source.as(FileProvider.class).getFile(); | source.as(FileProvider.class).getFile(); | ||||
| try { | try { | ||||
| copyUsingFileChannels(sourceFile, destFile); | |||||
| copyUsingFileChannels(sourceFile, destFile, project); | |||||
| copied = true; | copied = true; | ||||
| } catch (final IOException ex) { | } catch (final IOException ex) { | ||||
| String msg = "Attempt to copy " + sourceFile | String msg = "Attempt to copy " + sourceFile | ||||
| @@ -666,6 +666,13 @@ public class ResourceUtils { | |||||
| final String outputEncoding, | final String outputEncoding, | ||||
| final Project project) | final Project project) | ||||
| throws IOException { | 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; | |||||
| } | |||||
| BufferedReader in = null; | BufferedReader in = null; | ||||
| BufferedWriter out = null; | BufferedWriter out = null; | ||||
| try { | try { | ||||
| @@ -724,6 +731,13 @@ public class ResourceUtils { | |||||
| final String outputEncoding, | final String outputEncoding, | ||||
| final Project project) | final Project project) | ||||
| throws IOException { | 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; | |||||
| } | |||||
| BufferedReader in = null; | BufferedReader in = null; | ||||
| BufferedWriter out = null; | BufferedWriter out = null; | ||||
| try { | try { | ||||
| @@ -767,9 +781,14 @@ public class ResourceUtils { | |||||
| } | } | ||||
| private static void copyUsingFileChannels(final File sourceFile, | private static void copyUsingFileChannels(final File sourceFile, | ||||
| final File destFile) | |||||
| final File destFile, final Project project) | |||||
| throws IOException { | 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(); | final File parent = destFile.getParentFile(); | ||||
| if (parent != null && !parent.isDirectory() | if (parent != null && !parent.isDirectory() | ||||
| && !(parent.mkdirs() || parent.isDirectory())) { | && !(parent.mkdirs() || parent.isDirectory())) { | ||||
| @@ -807,6 +826,13 @@ public class ResourceUtils { | |||||
| private static void copyUsingStreams(final Resource source, final Resource dest, | private static void copyUsingStreams(final Resource source, final Resource dest, | ||||
| final boolean append, final Project project) | final boolean append, final Project project) | ||||
| throws IOException { | 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; | |||||
| } | |||||
| InputStream in = null; | InputStream in = null; | ||||
| OutputStream out = null; | OutputStream out = null; | ||||
| try { | try { | ||||
| @@ -843,6 +869,33 @@ public class ResourceUtils { | |||||
| return resource.getOutputStream(); | 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 { | public interface ResourceSelectorProvider { | ||||
| ResourceSelector getTargetSelectorForSource(Resource source); | ResourceSelector getTargetSelectorForSource(Resource source); | ||||
| } | } | ||||
| @@ -21,12 +21,16 @@ package org.apache.tools.ant.taskdefs; | |||||
| import org.apache.tools.ant.BuildException; | import org.apache.tools.ant.BuildException; | ||||
| import org.apache.tools.ant.BuildFileRule; | import org.apache.tools.ant.BuildFileRule; | ||||
| import org.apache.tools.ant.FileUtilities; | import org.apache.tools.ant.FileUtilities; | ||||
| import org.apache.tools.ant.taskdefs.condition.Os; | |||||
| import org.apache.tools.ant.util.FileUtils; | import org.apache.tools.ant.util.FileUtils; | ||||
| import org.junit.Assert; | |||||
| import org.junit.Assume; | |||||
| import org.junit.Before; | import org.junit.Before; | ||||
| import org.junit.Ignore; | import org.junit.Ignore; | ||||
| import org.junit.Rule; | import org.junit.Rule; | ||||
| import org.junit.Test; | import org.junit.Test; | ||||
| import java.io.BufferedReader; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.FileReader; | import java.io.FileReader; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| @@ -189,7 +193,7 @@ public class CopyTest { | |||||
| assertTrue(ex.getMessage().endsWith(" does not exist.")); | assertTrue(ex.getMessage().endsWith(" does not exist.")); | ||||
| } | } | ||||
| } | } | ||||
| @Test | @Test | ||||
| public void testFileResourcePlain() { | public void testFileResourcePlain() { | ||||
| buildRule.executeTarget("testFileResourcePlain"); | buildRule.executeTarget("testFileResourcePlain"); | ||||
| @@ -212,7 +216,7 @@ public class CopyTest { | |||||
| assertTrue(file2.exists()); | assertTrue(file2.exists()); | ||||
| assertTrue(file3.exists()); | assertTrue(file3.exists()); | ||||
| } | } | ||||
| @Test | @Test | ||||
| public void testFileResourceWithFilter() { | public void testFileResourceWithFilter() { | ||||
| buildRule.executeTarget("testFileResourceWithFilter"); | buildRule.executeTarget("testFileResourceWithFilter"); | ||||
| @@ -225,7 +229,7 @@ public class CopyTest { | |||||
| // no-op: not a real business error | // no-op: not a real business error | ||||
| } | } | ||||
| } | } | ||||
| @Test | @Test | ||||
| public void testPathAsResource() { | public void testPathAsResource() { | ||||
| buildRule.executeTarget("testPathAsResource"); | buildRule.executeTarget("testPathAsResource"); | ||||
| @@ -236,7 +240,7 @@ public class CopyTest { | |||||
| assertTrue(file2.exists()); | assertTrue(file2.exists()); | ||||
| assertTrue(file3.exists()); | assertTrue(file3.exists()); | ||||
| } | } | ||||
| @Test | @Test | ||||
| public void testZipfileset() { | public void testZipfileset() { | ||||
| buildRule.executeTarget("testZipfileset"); | buildRule.executeTarget("testZipfileset"); | ||||
| @@ -252,7 +256,7 @@ public class CopyTest { | |||||
| public void testDirset() { | public void testDirset() { | ||||
| buildRule.executeTarget("testDirset"); | buildRule.executeTarget("testDirset"); | ||||
| } | } | ||||
| @Ignore("Previously ignored due to naming convention") | @Ignore("Previously ignored due to naming convention") | ||||
| @Test | @Test | ||||
| public void testResourcePlain() { | public void testResourcePlain() { | ||||
| @@ -276,5 +280,60 @@ public class CopyTest { | |||||
| public void testOnlineResources() { | public void testOnlineResources() { | ||||
| buildRule.executeTarget("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 <a href="https://bz.apache.org/bugzilla/show_bug.cgi?id=60644">issue 60644</a> | |||||
| */ | |||||
| @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); | |||||
| } | |||||
| } | } | ||||