Browse Source

BZ-60644 Skip copying files, to avoid corrupting source files, if the source file and dest file are symlinked to each other

master
Jaikiran Pai 8 years ago
parent
commit
20f9491557
3 changed files with 155 additions and 17 deletions
  1. +27
    -0
      src/etc/testcases/taskdefs/copy.xml
  2. +63
    -11
      src/main/org/apache/tools/ant/util/ResourceUtils.java
  3. +65
    -6
      src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java

+ 27
- 0
src/etc/testcases/taskdefs/copy.xml View File

@@ -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>

+ 63
- 11
src/main/org/apache/tools/ant/util/ResourceUtils.java View File

@@ -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);
} }


+ 65
- 6
src/tests/junit/org/apache/tools/ant/taskdefs/CopyTest.java View File

@@ -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);
}
} }

Loading…
Cancel
Save