diff --git a/src/etc/testcases/taskdefs/sync.xml b/src/etc/testcases/taskdefs/sync.xml index d2eb126e7..27f8952e1 100644 --- a/src/etc/testcases/taskdefs/sync.xml +++ b/src/etc/testcases/taskdefs/sync.xml @@ -138,4 +138,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/org/apache/tools/ant/taskdefs/Sync.java b/src/main/org/apache/tools/ant/taskdefs/Sync.java index 32863c3a7..25fc00e83 100644 --- a/src/main/org/apache/tools/ant/taskdefs/Sync.java +++ b/src/main/org/apache/tools/ant/taskdefs/Sync.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -40,6 +41,7 @@ import org.apache.tools.ant.types.resources.Restrict; import org.apache.tools.ant.types.resources.selectors.Exists; import org.apache.tools.ant.types.selectors.FileSelector; import org.apache.tools.ant.types.selectors.NoneSelector; +import org.apache.tools.ant.util.FileUtils; /** * Synchronize a local target directory from the files defined @@ -222,6 +224,13 @@ public class Sync extends Task { } else { ds = new DirectoryScanner(); ds.setBasedir(toDir); + // set the case sensitivity of the directory scanner based on the + // directory we are scanning, if we are able to determine that detail. + // Else let the directory scanner default it to whatever it does internally + final Optional caseSensitive = FileUtils.isCaseSensitiveFileSystem(toDir.toPath()); + if (caseSensitive.isPresent()) { + ds.setCaseSensitive(caseSensitive.get()); + } } ds.addExcludes(excls); diff --git a/src/main/org/apache/tools/ant/util/FileUtils.java b/src/main/org/apache/tools/ant/util/FileUtils.java index 7764cebc5..d66f54789 100644 --- a/src/main/org/apache/tools/ant/util/FileUtils.java +++ b/src/main/org/apache/tools/ant/util/FileUtils.java @@ -31,6 +31,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.channels.Channel; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -38,7 +39,11 @@ import java.nio.file.StandardOpenOption; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.Stack; import java.util.StringTokenizer; @@ -75,6 +80,7 @@ public class FileUtils { private static final boolean ON_DOS = Os.isFamily("dos"); private static final boolean ON_WIN9X = Os.isFamily("win9x"); private static final boolean ON_WINDOWS = Os.isFamily("windows"); + private static final Map fileSystemCaseSensitivity = new HashMap<>(); static final int BUF_SIZE = 8192; @@ -1769,4 +1775,67 @@ public class FileUtils { return Files.newOutputStream(path); } } + + /** + * Tries to determine the case sensitivity of the filesystem corresponding to the + * {@code path}. While doing so, this method might create a temporary file under + * the directory represented by the {@code path}, if it's a directory or in the + * parent directory of the {@code path}, if it's a file. + *

+ * This method works on a best effort basis and will return an {@link Optional#empty()} + * if it cannot determine the case sensitivity, either due to exception or for any other + * reason. + *

+ * @param path The path whose filesystem case sensitivity needs to be checked + * @return Returns true if the filesystem corresponding to the passed {@code path} + * is case sensitive. Else returns false. If the case sensitivity + * cannot be determined for whatever reason, this method returns an + * {@link Optional#empty()} + * @throws IllegalArgumentException If the passed path is null + * @since Ant 1.10.6 + */ + public static Optional isCaseSensitiveFileSystem(final Path path) { + if (path == null) { + throw new IllegalArgumentException("Path cannot be null"); + } + final FileSystem fileSystem = path.getFileSystem(); + final Boolean caseSensitivity = fileSystemCaseSensitivity.get(fileSystem); + if (caseSensitivity != null) { + return Optional.of(caseSensitivity); + } + final String mixedCaseFileNamePrefix = "aNt"; + Path mixedCaseTmpFile = null; + final boolean caseSensitive; + try { + synchronized (fileSystemCaseSensitivity) { + if (fileSystemCaseSensitivity.containsKey(fileSystem)) { + return Optional.of(fileSystemCaseSensitivity.get(fileSystem)); + } + if (Files.isRegularFile(path)) { + mixedCaseTmpFile = Files.createTempFile(path.getParent(), mixedCaseFileNamePrefix, null); + } else if (Files.isDirectory(path)) { + mixedCaseTmpFile = Files.createTempFile(path, mixedCaseFileNamePrefix, null); + } else { + // we can only do our tricks to figure out whether the filesystem is + // case sensitive, only if the path is a directory or a file. + // In other cases (like the path being non-existent), we don't + // have a way to determine that detail + return Optional.empty(); + } + final Path lowerCasePath = Paths.get(mixedCaseTmpFile.toString().toLowerCase(Locale.US)); + caseSensitive = !Files.isSameFile(mixedCaseTmpFile, lowerCasePath); + fileSystemCaseSensitivity.put(fileSystem, caseSensitive); + } + } catch (IOException ioe) { + System.err.println("Could not determine the case sensitivity of the " + + "filesystem for path " + path + " due to " + ioe); + return Optional.empty(); + } finally { + // delete the tmp file + if (mixedCaseTmpFile != null) { + FileUtils.delete(mixedCaseTmpFile.toFile()); + } + } + return Optional.of(caseSensitive); + } } diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/SyncTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/SyncTest.java index e98b56d60..7a77526a2 100644 --- a/src/tests/junit/org/apache/tools/ant/taskdefs/SyncTest.java +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/SyncTest.java @@ -139,6 +139,21 @@ public class SyncTest { assertThat(buildRule.getFullLog(), not(containsString("Removing orphan file:"))); } + /** + * Test for bz-62890 bug fix + */ + @Test + public void testCaseSensitivityOfDest() { + buildRule.executeTarget("casesensitivity-test"); + final String destDir = buildRule.getProject().getProperty("dest") + "/casecheck"; + assertFileIsPresent(destDir + "/a.txt"); + assertFileIsPresent(destDir + "/A.txt"); + assertFileIsPresent(destDir + "/foo.txt"); + + assertFileIsNotPresent(destDir + "/bar.txt"); + } + + public void assertFileIsPresent(String f) { assertTrue("Expected file " + f, buildRule.getProject().resolveFile(f).exists()); } diff --git a/src/tests/junit/org/apache/tools/ant/util/FileUtilsTest.java b/src/tests/junit/org/apache/tools/ant/util/FileUtilsTest.java index f602a25af..d9ef5eadd 100644 --- a/src/tests/junit/org/apache/tools/ant/util/FileUtilsTest.java +++ b/src/tests/junit/org/apache/tools/ant/util/FileUtilsTest.java @@ -21,11 +21,18 @@ package org.apache.tools.ant.util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.Optional; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.MagicTestNames; import org.apache.tools.ant.taskdefs.condition.Os; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -681,6 +688,37 @@ public class FileUtilsTest { assertTrue(FILE_UTILS.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\"), false)); } + /** + * Tests {@link FileUtils#isCaseSensitiveFileSystem(Path)} method + * + * @throws Exception + */ + @Test + public void testCaseSensitiveFileSystem() throws Exception { + // create a temp file in a fresh directory + final Path tmpDir = Files.createTempDirectory(null); + final Path tmpFile = Files.createTempFile(tmpDir, null, null); + tmpFile.toFile().deleteOnExit(); + tmpDir.toFile().deleteOnExit(); + // now check if a file with that same name but different case is considered to exist + final boolean existsAsLowerCase = Files.exists(Paths.get(tmpDir.toString(), tmpFile.getFileName().toString().toLowerCase(Locale.US))); + final boolean existsAsUpperCase = Files.exists(Paths.get(tmpDir.toString(), tmpFile.getFileName().toString().toUpperCase(Locale.US))); + // if the temp file that we created is found to not exist in a particular "case", then + // the filesystem is case sensitive + final Boolean expectedCaseSensitivity = existsAsLowerCase == false || existsAsUpperCase == false; + + // call the method and pass it a directory + Optional actualCaseSensitivity = FileUtils.isCaseSensitiveFileSystem(tmpDir); + Assert.assertTrue("Filesystem case sensitivity was expected to be determined", actualCaseSensitivity.isPresent()); + Assert.assertEquals("Filesystem was expected to be case " + (expectedCaseSensitivity + ? "sensitive" : "insensitive"), expectedCaseSensitivity, actualCaseSensitivity.get()); + + // now test it out by passing it a file + actualCaseSensitivity = FileUtils.isCaseSensitiveFileSystem(tmpFile); + Assert.assertTrue("Filesystem case sensitivity was expected to be determined", actualCaseSensitivity.isPresent()); + Assert.assertEquals("Filesystem was expected to be case " + (expectedCaseSensitivity + ? "sensitive" : "insensitive"), expectedCaseSensitivity, actualCaseSensitivity.get()); + } /** * adapt file separators to local conventions */