@@ -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> |
@@ -417,7 +417,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 | ||||
@@ -641,11 +641,17 @@ public class ResourceUtils { | |||||
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; | |||||
} | |||||
try (Reader in = filterWith(project, inputEncoding, filterChains, | 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(); | final LineTokenizer lineTokenizer = new LineTokenizer(); | ||||
lineTokenizer.setIncludeDelims(true); | lineTokenizer.setIncludeDelims(true); | ||||
@@ -691,11 +697,18 @@ 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; | |||||
} | |||||
try (Reader in = filterWith(project, inputEncoding, filterChains, | 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]; | final char[] buffer = new char[FileUtils.BUF_SIZE]; | ||||
while (true) { | while (true) { | ||||
final int nRead = in.read(buffer, 0, buffer.length); | final int nRead = in.read(buffer, 0, buffer.length); | ||||
@@ -705,12 +718,18 @@ public class ResourceUtils { | |||||
out.write(buffer, 0, nRead); | out.write(buffer, 0, nRead); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
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())) { | ||||
@@ -738,8 +757,14 @@ 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; | |||||
} | |||||
try (InputStream in = source.getInputStream(); | 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]; | final byte[] buffer = new byte[FileUtils.BUF_SIZE]; | ||||
int count = 0; | int count = 0; | ||||
@@ -768,6 +793,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); | |||||
} | |||||
} | } |