| @@ -265,4 +265,31 @@ a=b= | |||
| </fail> | |||
| </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> | |||
| @@ -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); | |||
| } | |||
| @@ -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 <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); | |||
| } | |||
| } | |||