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