@@ -138,4 +138,23 @@ | |||||
</sync> | </sync> | ||||
</target> | </target> | ||||
<target name="casesensitivity-test" depends="setUp" description="Tests the bug fix for bz-62890"> | |||||
<mkdir dir="${src}/casecheck"/> | |||||
<mkdir dir="${dest}/casecheck"/> | |||||
<!-- lower case file in source dir --> | |||||
<touch file="${src}/casecheck/a.txt"/> | |||||
<!-- upper case file in dest dir --> | |||||
<touch file="${dest}/casecheck/A.txt"/> | |||||
<!-- some random file in source dir --> | |||||
<touch file="${src}/casecheck/foo.txt"/> | |||||
<!-- some random file in dest dir --> | |||||
<touch file="${dest}/casecheck/bar.txt"/> | |||||
<sync todir="${dest}/casecheck" | |||||
includeEmptyDirs="true" | |||||
overwrite="false"> | |||||
<fileset dir="${src}/casecheck"/> | |||||
</sync> | |||||
</target> | |||||
</project> | </project> |
@@ -23,6 +23,7 @@ import java.util.Collections; | |||||
import java.util.HashSet; | import java.util.HashSet; | ||||
import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Optional; | |||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.stream.Stream; | 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.resources.selectors.Exists; | ||||
import org.apache.tools.ant.types.selectors.FileSelector; | import org.apache.tools.ant.types.selectors.FileSelector; | ||||
import org.apache.tools.ant.types.selectors.NoneSelector; | import org.apache.tools.ant.types.selectors.NoneSelector; | ||||
import org.apache.tools.ant.util.FileUtils; | |||||
/** | /** | ||||
* Synchronize a local target directory from the files defined | * Synchronize a local target directory from the files defined | ||||
@@ -222,6 +224,13 @@ public class Sync extends Task { | |||||
} else { | } else { | ||||
ds = new DirectoryScanner(); | ds = new DirectoryScanner(); | ||||
ds.setBasedir(toDir); | 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<Boolean> caseSensitive = FileUtils.isCaseSensitiveFileSystem(toDir.toPath()); | |||||
if (caseSensitive.isPresent()) { | |||||
ds.setCaseSensitive(caseSensitive.get()); | |||||
} | |||||
} | } | ||||
ds.addExcludes(excls); | ds.addExcludes(excls); | ||||
@@ -31,6 +31,7 @@ import java.net.MalformedURLException; | |||||
import java.net.URL; | import java.net.URL; | ||||
import java.net.URLConnection; | import java.net.URLConnection; | ||||
import java.nio.channels.Channel; | import java.nio.channels.Channel; | ||||
import java.nio.file.FileSystem; | |||||
import java.nio.file.Files; | import java.nio.file.Files; | ||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.nio.file.Paths; | import java.nio.file.Paths; | ||||
@@ -38,7 +39,11 @@ import java.nio.file.StandardOpenOption; | |||||
import java.text.DecimalFormat; | import java.text.DecimalFormat; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.HashMap; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Locale; | |||||
import java.util.Map; | |||||
import java.util.Optional; | |||||
import java.util.Random; | import java.util.Random; | ||||
import java.util.Stack; | import java.util.Stack; | ||||
import java.util.StringTokenizer; | 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_DOS = Os.isFamily("dos"); | ||||
private static final boolean ON_WIN9X = Os.isFamily("win9x"); | private static final boolean ON_WIN9X = Os.isFamily("win9x"); | ||||
private static final boolean ON_WINDOWS = Os.isFamily("windows"); | private static final boolean ON_WINDOWS = Os.isFamily("windows"); | ||||
private static final Map<FileSystem, Boolean> fileSystemCaseSensitivity = new HashMap<>(); | |||||
static final int BUF_SIZE = 8192; | static final int BUF_SIZE = 8192; | ||||
@@ -1769,4 +1775,67 @@ public class FileUtils { | |||||
return Files.newOutputStream(path); | 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. | |||||
* <p> | |||||
* 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. | |||||
* </p> | |||||
* @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<Boolean> 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); | |||||
} | |||||
} | } |
@@ -139,6 +139,21 @@ public class SyncTest { | |||||
assertThat(buildRule.getFullLog(), not(containsString("Removing orphan file:"))); | 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) { | public void assertFileIsPresent(String f) { | ||||
assertTrue("Expected file " + f, buildRule.getProject().resolveFile(f).exists()); | assertTrue("Expected file " + f, buildRule.getProject().resolveFile(f).exists()); | ||||
} | } | ||||
@@ -21,11 +21,18 @@ package org.apache.tools.ant.util; | |||||
import java.io.File; | import java.io.File; | ||||
import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||
import java.io.IOException; | 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.BuildException; | ||||
import org.apache.tools.ant.MagicTestNames; | import org.apache.tools.ant.MagicTestNames; | ||||
import org.apache.tools.ant.taskdefs.condition.Os; | import org.apache.tools.ant.taskdefs.condition.Os; | ||||
import org.junit.After; | import org.junit.After; | ||||
import org.junit.Assert; | |||||
import org.junit.Before; | import org.junit.Before; | ||||
import org.junit.Rule; | import org.junit.Rule; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
@@ -681,6 +688,37 @@ public class FileUtilsTest { | |||||
assertTrue(FILE_UTILS.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\"), false)); | 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<Boolean> 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 | * adapt file separators to local conventions | ||||
*/ | */ | ||||