Skip to content

Commit 94da6de

Browse files
committed
CopyDirectoryVisitor creates incorrect file names when copying between
different file systems that use different file system separators ("/" versus "\") - Fixes PathUtils.copyDirectory(Path, Path, CopyOption...)
1 parent 4e45c01 commit 94da6de

File tree

4 files changed

+42
-14
lines changed

4 files changed

+42
-14
lines changed

src/changes/changes.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ The <action> type attribute can be add,update,fix,remove.
7373
<action dev="ggregory" type="fix" due-to="Gary Gregory">Fix Javadoc for ChunkedOutputStream.Builder.</action>
7474
<action dev="ggregory" type="fix" due-to="Gary Gregory">General Javadoc improvements.</action>
7575
<action dev="ggregory" type="fix" due-to="Gary Gregory">Calling QueueInputStream.QueueInputStream(null) maps to the same kind of default blocking queue as QueueInputStream.Builder.setBlockingQueue(null).</action>
76+
<action dev="ggregory" type="fix" due-to="Gary Gregory">CopyDirectoryVisitor creates incorrect file names when copying between different file systems that use different file system separators ("/" versus "\"); fixes PathUtils.copyDirectory(Path, Path, CopyOption...).</action>
7677
<!-- ADD -->
7778
<action dev="ggregory" type="add" issue="IO-860" due-to="Nico Strecker, Gary Gregory">Add ThrottledInputStream.Builder.setMaxBytes(long, ChronoUnit).</action>
7879
<action dev="ggregory" type="add" due-to="Gary Gregory">Add IOIterable.</action>

src/main/java/org/apache/commons/io/file/CopyDirectoryVisitor.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.IOException;
2121
import java.nio.file.CopyOption;
22+
import java.nio.file.FileSystem;
2223
import java.nio.file.FileVisitResult;
2324
import java.nio.file.Files;
2425
import java.nio.file.Path;
@@ -141,8 +142,7 @@ public int hashCode() {
141142
final int prime = 31;
142143
int result = super.hashCode();
143144
result = prime * result + Arrays.hashCode(copyOptions);
144-
result = prime * result + Objects.hash(sourceDirectory, targetDirectory);
145-
return result;
145+
return prime * result + Objects.hash(sourceDirectory, targetDirectory);
146146
}
147147

148148
@Override
@@ -155,17 +155,30 @@ public FileVisitResult preVisitDirectory(final Path directory, final BasicFileAt
155155
return super.preVisitDirectory(directory, attributes);
156156
}
157157

158+
private Path resolve(final Path otherPath) {
159+
final FileSystem fileSystemTarget = targetDirectory.getFileSystem();
160+
final FileSystem fileSystemSource = sourceDirectory.getFileSystem();
161+
if (fileSystemTarget == fileSystemSource) {
162+
return targetDirectory.resolve(otherPath);
163+
}
164+
final String separatorSource = fileSystemSource.getSeparator();
165+
final String separatorTarget = fileSystemTarget.getSeparator();
166+
final String otherString = otherPath.toString();
167+
return targetDirectory.resolve(Objects.equals(separatorSource, separatorTarget) ? otherString : otherString.replace(separatorSource, separatorTarget));
168+
}
169+
158170
/**
159171
* Relativizes against {@code sourceDirectory}, then resolves against {@code targetDirectory}.
160-
*
161-
* We have to call {@link Path#toString()} relative value because we cannot use paths belonging to different
162-
* FileSystems in the Path methods, usually this leads to {@link ProviderMismatchException}.
172+
* <p>
173+
* We call {@link Path#toString()} on the relativized value because we cannot use paths from different FileSystems which throws
174+
* {@link ProviderMismatchException}. Special care is taken to handle differences in file system separators.
175+
* </p>
163176
*
164177
* @param directory the directory to relativize.
165178
* @return a new path, relativized against sourceDirectory, then resolved against targetDirectory.
166179
*/
167180
private Path resolveRelativeAsString(final Path directory) {
168-
return targetDirectory.resolve(sourceDirectory.relativize(directory).toString());
181+
return resolve(sourceDirectory.relativize(directory));
169182
}
170183

171184
@Override

src/main/java/org/apache/commons/io/file/PathUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ private static boolean equalsIgnoreFileSystem(final Path path1, final Path path2
129129
final String separator2 = fileSystem2.getSeparator();
130130
final String string1 = path1.toString();
131131
final String string2 = path2.toString();
132-
if (separator1.equals(separator2)) {
132+
if (Objects.equals(separator1, separator2)) {
133133
// Separators are the same, so we can use toString comparison
134-
return string1.equals(string2);
134+
return Objects.equals(string1, string2);
135135
}
136136
// Compare paths from different file systems component by component.
137137
return extractKey(separator1, string1).equals(extractKey(separator2, string2));

src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
*/
4545
public class PathUtilsContentEqualsTest {
4646

47-
static Configuration[] testContentEqualsFileSystemsMemVsZip() {
47+
static Configuration[] testConfigurations() {
4848
// @formatter:off
4949
return new Configuration[] {
5050
Configuration.osX().toBuilder().setWorkingDirectory("/").build(),
@@ -104,13 +104,27 @@ private String getName() {
104104
}
105105

106106
@ParameterizedTest
107-
@MethodSource
107+
@MethodSource("testConfigurations")
108+
public void testContentEqualsFileSystemsMemVsMem(final Configuration configuration) throws Exception {
109+
final Path refDir = Paths.get("src/test/resources/dir-equals-tests");
110+
try (FileSystem fileSystem1 = Jimfs.newFileSystem(configuration);
111+
FileSystem fileSystem2 = Jimfs.newFileSystem(configuration)) {
112+
final Path fsDir1 = fileSystem1.getPath(refDir.getFileName().toString());
113+
final Path fsDir2 = fileSystem2.getPath(refDir.getFileName().toString());
114+
assertTrue(PathUtils.copyDirectory(refDir, fsDir1).getByteCounter().get() > 0);
115+
assertTrue(PathUtils.copyDirectory(refDir, fsDir2).getByteCounter().get() > 0);
116+
assertContentEquals(fileSystem1, fileSystem2);
117+
}
118+
}
119+
120+
@ParameterizedTest
121+
@MethodSource("testConfigurations")
108122
public void testContentEqualsFileSystemsMemVsZip(final Configuration configuration) throws Exception {
109-
final Path dir1 = Paths.get("src/test/resources/dir-equals-tests");
123+
final Path refDir = Paths.get("src/test/resources/dir-equals-tests");
110124
try (FileSystem fileSystem1 = Jimfs.newFileSystem(configuration);
111-
FileSystem fileSystem2 = FileSystems.newFileSystem(dir1.resolveSibling(dir1.getFileName() + ".zip"), null)) {
112-
final Path dir2 = fileSystem1.getPath(dir1.getFileName().toString());
113-
final PathCounters copyDirectory = PathUtils.copyDirectory(dir1, dir2);
125+
FileSystem fileSystem2 = FileSystems.newFileSystem(refDir.resolveSibling(refDir.getFileName() + ".zip"), null)) {
126+
final Path fsDir1 = fileSystem1.getPath(refDir.getFileName().toString());
127+
final PathCounters copyDirectory = PathUtils.copyDirectory(refDir, fsDir1);
114128
assertTrue(copyDirectory.getByteCounter().get() > 0);
115129
assertContentEquals(fileSystem1, fileSystem2);
116130
}

0 commit comments

Comments
 (0)