This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-io.git


The following commit(s) were added to refs/heads/master by this push:
     new f4b721c63 [IO-872] PathUtils.directoryAndFileContentEquals doesn't 
work across FileSystems
f4b721c63 is described below

commit f4b721c63e9e1db2f72dbf44d2c651f380391367
Author: Gary D. Gregory <garydgreg...@gmail.com>
AuthorDate: Thu Apr 3 21:29:36 2025 -0400

    [IO-872] PathUtils.directoryAndFileContentEquals doesn't work across
    FileSystems
    
    - Add SimplePathVisitor.AbstractBuilder
    - Add CountingPathVisitor.AbstractBuilder and
    CountingPathVisitor.Builder
    - Add AccumulatorPathVisitor.Builder and builder(
    - For now, keep as much of the new code private or package-private as
    possible
---
 src/changes/changes.xml                            |   3 +
 src/main/java/org/apache/commons/io/FileUtils.java |  10 +-
 .../commons/io/file/AccumulatorPathVisitor.java    |  56 +++++++-
 .../commons/io/file/CountingPathVisitor.java       | 159 +++++++++++++++++----
 .../java/org/apache/commons/io/file/PathUtils.java |  75 +++++++++-
 .../apache/commons/io/file/SimplePathVisitor.java  |  47 +++++-
 .../io/file/AccumulatorPathVisitorTest.java        |   2 +-
 .../io/file/PathUtilsContentEqualsTest.java        |   4 +-
 8 files changed, 310 insertions(+), 46 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index c690882d0..b94b1676f 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -90,6 +90,9 @@ The <action> type attribute can be add,update,fix,remove.
       <action dev="ggregory" type="add"                due-to="Gary 
Gregory">Add Uncheck.getAsBoolean(IOBooleanSupplier).</action>
       <action dev="ggregory" type="add"                due-to="Gary 
Gregory">Add FileChannels.contentEquals(SeekableByteChannel, 
SeekableByteChannel, int).</action>
       <action dev="ggregory" type="add"                due-to="Gary 
Gregory">Add FileChannels.contentEquals(ReadableByteChannel, 
ReadableByteChannel, int).</action>
+      <action dev="ggregory" type="add" issue="IO-872" due-to="Gary 
Gregory">Add SimplePathVisitor.AbstractBuilder.</action>
+      <action dev="ggregory" type="add" issue="IO-872" due-to="Gary 
Gregory">Add CountingPathVisitor.AbstractBuilder and 
CountingPathVisitor.Builder.</action>
+      <action dev="ggregory" type="add" issue="IO-872" due-to="Gary 
Gregory">Add AccumulatorPathVisitor.Builder and builder().</action>
       <!-- UPDATE -->
       <action dev="ggregory" type="update"             due-to="Dependabot, 
Gary Gregory">Bump commons.bytebuddy.version from 1.15.10 to 1.17.5 #710, #715, 
#720, #734, #735.</action>
       <action dev="ggregory" type="update"             due-to="Gary 
Gregory">Bump commons-codec:commons-codec from 1.17.1 to 1.18.0. #717.</action>
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java 
b/src/main/java/org/apache/commons/io/FileUtils.java
index 701791e26..e04977670 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -2282,8 +2282,14 @@ private static AccumulatorPathVisitor 
listAccumulate(final File directory, final
         final boolean isDirFilterSet = dirFilter != null;
         final FileEqualsFileFilter rootDirFilter = new 
FileEqualsFileFilter(directory);
         final PathFilter dirPathFilter = isDirFilterSet ? 
rootDirFilter.or(dirFilter) : rootDirFilter;
-        final AccumulatorPathVisitor visitor = new 
AccumulatorPathVisitor(Counters.noopPathCounters(), fileFilter, dirPathFilter,
-                (p, e) -> FileVisitResult.CONTINUE);
+        // @formatter:off
+        final AccumulatorPathVisitor visitor = AccumulatorPathVisitor.builder()
+                .setPathCounters(Counters.noopPathCounters())
+                .setFileFilter(fileFilter)
+                .setDirectoryFilter(dirPathFilter)
+                .setVisitFileFailedFunction((p, e) -> FileVisitResult.CONTINUE)
+                .get();
+        // @formatter:on
         final Set<FileVisitOption> optionSet = new HashSet<>();
         if (options != null) {
             Collections.addAll(optionSet, options);
diff --git 
a/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java 
b/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java
index 0d4bf2d91..c8e3e06af 100644
--- a/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/AccumulatorPathVisitor.java
@@ -60,13 +60,38 @@
  */
 public class AccumulatorPathVisitor extends CountingPathVisitor {
 
+    /**
+     * Builds instances of {@link AccumulatorPathVisitor}.
+     *
+     * @since 2.18.0
+     */
+    public static class Builder extends 
AbstractBuilder<AccumulatorPathVisitor, Builder> {
+        @Override
+        public AccumulatorPathVisitor get() {
+            return new AccumulatorPathVisitor(this);
+        }
+
+    }
+
+    /**
+     * Builds instances of {@link AccumulatorPathVisitor}.
+     *
+     * @return a new builder.
+     * @since 2.18.0
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
     /**
      * Constructs a new instance configured with a BigInteger {@link 
PathCounters}.
      *
      * @return a new instance configured with a BigInteger {@link 
PathCounters}.
+     * @see #builder()
+     * @see Builder
      */
     public static AccumulatorPathVisitor withBigIntegerCounters() {
-        return new AccumulatorPathVisitor(Counters.bigIntegerPathCounters());
+        return 
builder().setPathCounters(Counters.bigIntegerPathCounters()).get();
     }
 
     /**
@@ -75,20 +100,23 @@ public static AccumulatorPathVisitor 
withBigIntegerCounters() {
      * @param fileFilter Filters files to accumulate and count.
      * @param dirFilter Filters directories to accumulate and count.
      * @return a new instance configured with a long {@link PathCounters}.
+     * @see #builder()
+     * @see Builder
      * @since 2.9.0
      */
-    public static AccumulatorPathVisitor withBigIntegerCounters(final 
PathFilter fileFilter,
-        final PathFilter dirFilter) {
-        return new AccumulatorPathVisitor(Counters.bigIntegerPathCounters(), 
fileFilter, dirFilter);
+    public static AccumulatorPathVisitor withBigIntegerCounters(final 
PathFilter fileFilter, final PathFilter dirFilter) {
+        return 
builder().setPathCounters(Counters.bigIntegerPathCounters()).setFileFilter(fileFilter).setDirectoryFilter(dirFilter).get();
     }
 
     /**
      * Constructs a new instance configured with a long {@link PathCounters}.
      *
      * @return a new instance configured with a long {@link PathCounters}.
+     * @see #builder()
+     * @see Builder
      */
     public static AccumulatorPathVisitor withLongCounters() {
-        return new AccumulatorPathVisitor(Counters.longPathCounters());
+        return builder().setPathCounters(Counters.longPathCounters()).get();
     }
 
     /**
@@ -97,10 +125,12 @@ public static AccumulatorPathVisitor withLongCounters() {
      * @param fileFilter Filters files to accumulate and count.
      * @param dirFilter Filters directories to accumulate and count.
      * @return a new instance configured with a long {@link PathCounters}.
+     * @see #builder()
+     * @see Builder
      * @since 2.9.0
      */
     public static AccumulatorPathVisitor withLongCounters(final PathFilter 
fileFilter, final PathFilter dirFilter) {
-        return new AccumulatorPathVisitor(Counters.longPathCounters(), 
fileFilter, dirFilter);
+        return 
builder().setPathCounters(Counters.longPathCounters()).setFileFilter(fileFilter).setDirectoryFilter(dirFilter).get();
     }
 
     private final List<Path> dirList = new ArrayList<>();
@@ -108,19 +138,27 @@ public static AccumulatorPathVisitor 
withLongCounters(final PathFilter fileFilte
     private final List<Path> fileList = new ArrayList<>();
 
     /**
-     * Constructs a new instance.
+     * Constructs a new instance with a noop path counter.
      *
      * @since 2.9.0
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     public AccumulatorPathVisitor() {
         super(Counters.noopPathCounters());
     }
 
+    private AccumulatorPathVisitor(final Builder builder) {
+        super(builder);
+    }
+
     /**
      * Constructs a new instance that counts file system elements.
      *
      * @param pathCounter How to count path visits.
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     public AccumulatorPathVisitor(final PathCounters pathCounter) {
         super(pathCounter);
     }
@@ -132,7 +170,9 @@ public AccumulatorPathVisitor(final PathCounters 
pathCounter) {
      * @param fileFilter Filters which files to count.
      * @param dirFilter Filters which directories to count.
      * @since 2.9.0
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     public AccumulatorPathVisitor(final PathCounters pathCounter, final 
PathFilter fileFilter, final PathFilter dirFilter) {
         super(pathCounter, fileFilter, dirFilter);
     }
@@ -145,7 +185,9 @@ public AccumulatorPathVisitor(final PathCounters 
pathCounter, final PathFilter f
      * @param dirFilter Filters which directories to count.
      * @param visitFileFailed Called on {@link #visitFileFailed(Path, 
IOException)}.
      * @since 2.12.0
+     * @deprecated Use {@link #builder()}.
      */
+    @Deprecated
     public AccumulatorPathVisitor(final PathCounters pathCounter, final 
PathFilter fileFilter, final PathFilter dirFilter,
         final IOBiFunction<Path, IOException, FileVisitResult> 
visitFileFailed) {
         super(pathCounter, fileFilter, dirFilter, visitFileFailed);
diff --git a/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java 
b/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java
index 00368ef90..8611cf5c7 100644
--- a/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java
@@ -24,6 +24,7 @@
 import java.nio.file.Path;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Objects;
+import java.util.function.UnaryOperator;
 
 import org.apache.commons.io.file.Counters.PathCounters;
 import org.apache.commons.io.filefilter.IOFileFilter;
@@ -38,23 +39,119 @@
  */
 public class CountingPathVisitor extends SimplePathVisitor {
 
+    /**
+     * Builds instances of {@link CountingPathVisitor}.
+     *
+     * @param <T> The CountingPathVisitor type.
+     * @param <B> The AbstractBuilder type.
+     * @since 2.18.0
+     */
+    public abstract static class AbstractBuilder<T, B extends 
AbstractBuilder<T, B>> extends SimplePathVisitor.AbstractBuilder<T, B> {
+
+        private PathCounters pathCounters = defaultPathCounters();
+        private PathFilter fileFilter = defaultFileFilter();
+        private PathFilter directoryFilter = defaultDirectoryFilter();
+        private UnaryOperator<Path> directoryPostTransformer = 
defaultDirectoryTransformer();
+
+        PathFilter getDirectoryFilter() {
+            return directoryFilter;
+        }
+
+        UnaryOperator<Path> getDirectoryPostTransformer() {
+            return directoryPostTransformer;
+        }
+
+        PathFilter getFileFilter() {
+            return fileFilter;
+        }
+
+        PathCounters getPathCounters() {
+            return pathCounters;
+        }
+
+        /**
+         * Sets how to filter directories.
+         *
+         * @param directoryFilter how to filter files.
+         * @return this instance.
+         */
+        public B setDirectoryFilter(final PathFilter directoryFilter) {
+            this.directoryFilter = directoryFilter != null ? directoryFilter : 
defaultDirectoryFilter();
+            return asThis();
+        }
+
+        /**
+         * Sets how to transform directories, defaults to {@link 
UnaryOperator#identity()}.
+         *
+         * @param directoryTransformer how to filter files.
+         * @return this instance.
+         */
+        public B setDirectoryPostTransformer(final UnaryOperator<Path> 
directoryTransformer) {
+            this.directoryPostTransformer = directoryTransformer != null ? 
directoryTransformer : defaultDirectoryTransformer();
+            return asThis();
+        }
+
+        /**
+         * Sets how to filter files.
+         *
+         * @param fileFilter how to filter files.
+         * @return this instance.
+         */
+        public B setFileFilter(final PathFilter fileFilter) {
+            this.fileFilter = fileFilter != null ? fileFilter : 
defaultFileFilter();
+            return asThis();
+        }
+
+        /**
+         * Sets how to count path visits.
+         *
+         * @param pathCounters How to count path visits.
+         * @return this instance.
+         */
+        public B setPathCounters(final PathCounters pathCounters) {
+            this.pathCounters = pathCounters != null ? pathCounters : 
defaultPathCounters();
+            return asThis();
+        }
+    }
+
+    /**
+     * Builds instances of {@link CountingPathVisitor}.
+     *
+     * @since 2.18.0
+     */
+    public static class Builder extends AbstractBuilder<CountingPathVisitor, 
Builder> {
+
+        @Override
+        public CountingPathVisitor get() {
+            return new CountingPathVisitor(this);
+        }
+    }
+
     static final String[] EMPTY_STRING_ARRAY = {};
 
-    static IOFileFilter defaultDirFilter() {
+    static IOFileFilter defaultDirectoryFilter() {
         return TrueFileFilter.INSTANCE;
     }
 
+    static UnaryOperator<Path> defaultDirectoryTransformer() {
+        return UnaryOperator.identity();
+    }
+
     static IOFileFilter defaultFileFilter() {
         return new SymbolicLinkFileFilter(FileVisitResult.TERMINATE, 
FileVisitResult.CONTINUE);
     }
 
+    static PathCounters defaultPathCounters() {
+        return Counters.longPathCounters();
+    }
+
     /**
      * Constructs a new instance configured with a {@link BigInteger} {@link 
PathCounters}.
      *
      * @return a new instance configured with a {@link BigInteger} {@link 
PathCounters}.
      */
     public static CountingPathVisitor withBigIntegerCounters() {
-        return new CountingPathVisitor(Counters.bigIntegerPathCounters());
+        return new 
Builder().setPathCounters(Counters.bigIntegerPathCounters()).get();
     }
 
     /**
@@ -63,51 +160,66 @@ public static CountingPathVisitor withBigIntegerCounters() 
{
      * @return a new instance configured with a {@code long} {@link 
PathCounters}.
      */
     public static CountingPathVisitor withLongCounters() {
-        return new CountingPathVisitor(Counters.longPathCounters());
+        return new 
Builder().setPathCounters(Counters.longPathCounters()).get();
     }
 
     private final PathCounters pathCounters;
     private final PathFilter fileFilter;
-    private final PathFilter dirFilter;
+    private final PathFilter directoryFilter;
+    private final UnaryOperator<Path> directoryPostTransformer;
+
+    CountingPathVisitor(final AbstractBuilder<?, ?> builder) {
+        super(builder);
+        this.pathCounters = builder.getPathCounters();
+        this.fileFilter = builder.getFileFilter();
+        this.directoryFilter = builder.getDirectoryFilter();
+        this.directoryPostTransformer = builder.getDirectoryPostTransformer();
+    }
 
     /**
      * Constructs a new instance.
      *
-     * @param pathCounter How to count path visits.
+     * @param pathCounters How to count path visits.
+     * @see Builder
      */
-    public CountingPathVisitor(final PathCounters pathCounter) {
-        this(pathCounter, defaultFileFilter(), defaultDirFilter());
+    public CountingPathVisitor(final PathCounters pathCounters) {
+        this(new Builder().setPathCounters(pathCounters));
     }
 
     /**
      * Constructs a new instance.
      *
-     * @param pathCounter How to count path visits.
-     * @param fileFilter Filters which files to count.
-     * @param dirFilter Filters which directories to count.
+     * @param pathCounters    How to count path visits.
+     * @param fileFilter      Filters which files to count.
+     * @param directoryFilter Filters which directories to count.
+     * @see Builder
      * @since 2.9.0
      */
-    public CountingPathVisitor(final PathCounters pathCounter, final 
PathFilter fileFilter, final PathFilter dirFilter) {
-        this.pathCounters = Objects.requireNonNull(pathCounter, "pathCounter");
+    public CountingPathVisitor(final PathCounters pathCounters, final 
PathFilter fileFilter, final PathFilter directoryFilter) {
+        this.pathCounters = Objects.requireNonNull(pathCounters, 
"pathCounters");
         this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
-        this.dirFilter = Objects.requireNonNull(dirFilter, "dirFilter");
+        this.directoryFilter = Objects.requireNonNull(directoryFilter, 
"directoryFilter");
+        this.directoryPostTransformer = UnaryOperator.identity();
     }
 
     /**
      * Constructs a new instance.
      *
-     * @param pathCounter How to count path visits.
-     * @param fileFilter Filters which files to count.
-     * @param dirFilter Filters which directories to count.
+     * @param pathCounters    How to count path visits.
+     * @param fileFilter      Filters which files to count.
+     * @param directoryFilter Filters which directories to count.
      * @param visitFileFailed Called on {@link #visitFileFailed(Path, 
IOException)}.
      * @since 2.12.0
+     * @deprecated Use {@link Builder}.
      */
-    public CountingPathVisitor(final PathCounters pathCounter, final 
PathFilter fileFilter, final PathFilter dirFilter,
-        final IOBiFunction<Path, IOException, FileVisitResult> 
visitFileFailed) {
+    @Deprecated
+    public CountingPathVisitor(final PathCounters pathCounters, final 
PathFilter fileFilter, final PathFilter directoryFilter,
+            final IOBiFunction<Path, IOException, FileVisitResult> 
visitFileFailed) {
         super(visitFileFailed);
-        this.pathCounters = Objects.requireNonNull(pathCounter, "pathCounter");
+        this.pathCounters = Objects.requireNonNull(pathCounters, 
"pathCounters");
         this.fileFilter = Objects.requireNonNull(fileFilter, "fileFilter");
-        this.dirFilter = Objects.requireNonNull(dirFilter, "dirFilter");
+        this.directoryFilter = Objects.requireNonNull(directoryFilter, 
"directoryFilter");
+        this.directoryPostTransformer = UnaryOperator.identity();
     }
 
     @Override
@@ -138,13 +250,13 @@ public int hashCode() {
 
     @Override
     public FileVisitResult postVisitDirectory(final Path dir, final 
IOException exc) throws IOException {
-        updateDirCounter(dir, exc);
+        updateDirCounter(directoryPostTransformer.apply(dir), exc);
         return FileVisitResult.CONTINUE;
     }
 
     @Override
     public FileVisitResult preVisitDirectory(final Path dir, final 
BasicFileAttributes attributes) throws IOException {
-        final FileVisitResult accept = dirFilter.accept(dir, attributes);
+        final FileVisitResult accept = directoryFilter.accept(dir, attributes);
         return accept != FileVisitResult.CONTINUE ? 
FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
     }
 
@@ -167,7 +279,7 @@ protected void updateDirCounter(final Path dir, final 
IOException exc) {
     /**
      * Updates the counters for visiting the given file.
      *
-     * @param file the visited file.
+     * @param file       the visited file.
      * @param attributes the visited file attributes.
      */
     protected void updateFileCounters(final Path file, final 
BasicFileAttributes attributes) {
@@ -183,5 +295,4 @@ public FileVisitResult visitFile(final Path file, final 
BasicFileAttributes attr
         }
         return FileVisitResult.CONTINUE;
     }
-
 }
diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java 
b/src/main/java/org/apache/commons/io/file/PathUtils.java
index 0d30404c4..679770f9b 100644
--- a/src/main/java/org/apache/commons/io/file/PathUtils.java
+++ b/src/main/java/org/apache/commons/io/file/PathUtils.java
@@ -30,6 +30,7 @@
 import java.nio.file.AccessDeniedException;
 import java.nio.file.CopyOption;
 import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
 import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.FileVisitor;
@@ -61,6 +62,7 @@
 import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -94,6 +96,51 @@ public final class PathUtils {
      */
     private static final class RelativeSortedPaths {
 
+        /**
+         * Compares lists of paths regardless of their file systems.
+         *
+         * @param list1 the first list.
+         * @param list2 the second list.
+         * @return whether the lists are equal.
+         */
+        private static boolean equals(final List<Path> list1, final List<Path> 
list2) {
+            if (list1.size() != list2.size()) {
+                return false;
+            }
+            // compare both lists using iterators
+            final Iterator<Path> iterator1 = list1.iterator();
+            final Iterator<Path> iterator2 = list2.iterator();
+            while (iterator1.hasNext() && iterator2.hasNext()) {
+                final Path path1 = iterator1.next();
+                final Path path2 = iterator2.next();
+                final FileSystem fileSystem1 = path1.getFileSystem();
+                final FileSystem fileSystem2 = path2.getFileSystem();
+                if (fileSystem1 == fileSystem2) {
+                    if (!path1.equals(path2)) {
+                        return false;
+                    }
+                } else if 
(fileSystem1.getSeparator().equals(fileSystem2.getSeparator())) {
+                    // Separators are the same, so we can use toString 
comparison
+                    if (!path1.toString().equals(path2.toString())) {
+                        return false;
+                    }
+                } else {
+                    // Compare paths from different file systems component by 
component.
+                    // Cant use toString() string comparison which may fail 
due to different path separators.
+                    final Iterator<Path> path1Iterator = path1.iterator();
+                    final Iterator<Path> path2Iterator = path2.iterator();
+                    while (path1Iterator.hasNext() && path2Iterator.hasNext()) 
{
+                        if 
(!path1Iterator.next().toString().equals(path2Iterator.next().toString())) {
+                            return false;
+                        }
+                    }
+                    // Check that both iterators are exhausted (paths have 
same number of components)
+                    return !path1Iterator.hasNext() && 
!path2Iterator.hasNext();
+                }
+            }
+            return true;
+        }
+
         final boolean equals;
         // final List<Path> relativeDirList1; // might need later?
         // final List<Path> relativeDirList2; // might need later?
@@ -133,12 +180,12 @@ private RelativeSortedPaths(final Path dir1, final Path 
dir2, final int maxDepth
                     } else {
                         tmpRelativeDirList1 = 
visitor1.relativizeDirectories(dir1, true, null);
                         tmpRelativeDirList2 = 
visitor2.relativizeDirectories(dir2, true, null);
-                        if (!tmpRelativeDirList1.equals(tmpRelativeDirList2)) {
+                        if (!equals(tmpRelativeDirList1, tmpRelativeDirList2)) 
{
                             equals = false;
                         } else {
                             tmpRelativeFileList1 = 
visitor1.relativizeFiles(dir1, true, null);
                             tmpRelativeFileList2 = 
visitor2.relativizeFiles(dir2, true, null);
-                            equals = 
tmpRelativeFileList1.equals(tmpRelativeFileList2);
+                            equals = equals(tmpRelativeFileList1, 
tmpRelativeFileList2);
                         }
                     }
                 }
@@ -223,7 +270,8 @@ private RelativeSortedPaths(final Path dir1, final Path 
dir2, final int maxDepth
      * @return file tree information.
      */
     private static AccumulatorPathVisitor accumulate(final Path directory, 
final int maxDepth, final FileVisitOption[] fileVisitOptions) throws 
IOException {
-        return visitFileTree(AccumulatorPathVisitor.withLongCounters(), 
directory, toFileVisitOptionSet(fileVisitOptions), maxDepth);
+        return 
visitFileTree(AccumulatorPathVisitor.builder().setDirectoryPostTransformer(PathUtils::stripTrailingSeparator).get(),
 directory,
+                toFileVisitOptionSet(fileVisitOptions), maxDepth);
     }
 
     /**
@@ -325,7 +373,7 @@ public static Path copyFileToDirectory(final Path 
sourceFile, final Path targetD
         // Path.resolve() naturally won't work across FileSystem unless we 
convert to a String
         final Path sourceFileName = 
Objects.requireNonNull(sourceFile.getFileName(), "source file name");
         final Path targetFile;
-        if (sourceFileName.getFileSystem() == targetDirectory.getFileSystem()) 
{
+        if (isSameFileSystem(sourceFileName, targetDirectory)) {
             targetFile = targetDirectory.resolve(sourceFileName);
         } else {
             targetFile = targetDirectory.resolve(sourceFileName.toString());
@@ -661,12 +709,17 @@ public static boolean directoryAndFileContentEquals(final 
Path path1, final Path
         // Both visitors contain the same normalized paths, we can compare 
file contents.
         final List<Path> fileList1 = relativeSortedPaths.relativeFileList1;
         final List<Path> fileList2 = relativeSortedPaths.relativeFileList2;
+        final boolean sameFileSystem = isSameFileSystem(path1, path2);
         for (final Path path : fileList1) {
-            final int binarySearch = Collections.binarySearch(fileList2, path);
+            final int binarySearch = sameFileSystem ? 
Collections.binarySearch(fileList2, path)
+                    : Collections.binarySearch(fileList2, path, 
Comparator.comparing(Path::toString));
             if (binarySearch <= -1) {
                 throw new IllegalStateException("Unexpected mismatch.");
             }
-            if (!fileContentEquals(path1.resolve(path), path2.resolve(path), 
linkOptions, openOptions)) {
+            if (sameFileSystem && !fileContentEquals(path1.resolve(path), 
path2.resolve(path), linkOptions, openOptions)) {
+                return false;
+            }
+            if (!fileContentEquals(path1.resolve(path.toString()), 
path2.resolve(path.toString()), linkOptions, openOptions)) {
                 return false;
             }
         }
@@ -1262,6 +1315,10 @@ public static boolean isRegularFile(final Path path, 
final LinkOption... options
         return path != null && Files.isRegularFile(path, options);
     }
 
+    static boolean isSameFileSystem(final Path path1, final Path path2) {
+        return path1.getFileSystem() == path2.getFileSystem();
+    }
+
     /**
      * Creates a new DirectoryStream for Paths rooted at the given directory.
      * <p>
@@ -1687,6 +1744,12 @@ public static BigInteger 
sizeOfDirectoryAsBigInteger(final Path directory) throw
         return 
countDirectoryAsBigInteger(directory).getByteCounter().getBigInteger();
     }
 
+    private static Path stripTrailingSeparator(final Path dir) {
+        final String separator = dir.getFileSystem().getSeparator();
+        final String fileName = dir.getFileName().toString();
+        return fileName.endsWith(separator) ? 
dir.resolveSibling(fileName.substring(0, fileName.length() - 1)) : dir;
+    }
+
     /**
      * Converts an array of {@link FileVisitOption} to a {@link Set}.
      *
diff --git a/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java 
b/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java
index 1c3b1ddcc..4762a126b 100644
--- a/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java
+++ b/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java
@@ -23,6 +23,7 @@
 import java.nio.file.SimpleFileVisitor;
 import java.util.Objects;
 
+import org.apache.commons.io.build.AbstractSupplier;
 import org.apache.commons.io.function.IOBiFunction;
 
 /**
@@ -32,6 +33,37 @@
  */
 public abstract class SimplePathVisitor extends SimpleFileVisitor<Path> 
implements PathVisitor {
 
+    /**
+     * Abstracts builder for subclasses.
+     *
+     * @param <T> The SimplePathVisitor type.
+     * @param <B> The builder type.
+     * @since 2.18.0
+     */
+    protected abstract static class AbstractBuilder<T, B extends 
AbstractSupplier<T, B>> extends AbstractSupplier<T, B> {
+
+        private IOBiFunction<Path, IOException, FileVisitResult> 
visitFileFailedFunction;
+
+        IOBiFunction<Path, IOException, FileVisitResult> 
getVisitFileFailedFunction() {
+            return visitFileFailedFunction;
+        }
+
+        /**
+         * Sets the function to call on {@link #visitFileFailed(Path, 
IOException)}.
+         * <p>
+         * Defaults to {@link SimpleFileVisitor#visitFileFailed(Object, 
IOException)} on construction.
+         * </p>
+         *
+         * @param visitFileFailedFunction the function to call on {@link 
#visitFileFailed(Path, IOException)}.
+         * @return this instance.
+         */
+        public B setVisitFileFailedFunction(final IOBiFunction<Path, 
IOException, FileVisitResult> visitFileFailedFunction) {
+            this.visitFileFailedFunction = visitFileFailedFunction;
+            return asThis();
+        }
+
+    }
+
     private final IOBiFunction<Path, IOException, FileVisitResult> 
visitFileFailedFunction;
 
     /**
@@ -44,10 +76,19 @@ protected SimplePathVisitor() {
     /**
      * Constructs a new instance.
      *
-     * @param visitFileFailed Called on {@link #visitFileFailed(Path, 
IOException)}.
+     * @param builder The builder provided by a subclass.
+     */
+    SimplePathVisitor(final AbstractBuilder<?, ?> builder) {
+        this.visitFileFailedFunction = builder.visitFileFailedFunction != null 
? builder.visitFileFailedFunction : super::visitFileFailed;
+    }
+
+    /**
+     * Constructs a new instance.
+     *
+     * @param visitFileFailedFunction Called on {@link #visitFileFailed(Path, 
IOException)}.
      */
-    protected SimplePathVisitor(final IOBiFunction<Path, IOException, 
FileVisitResult> visitFileFailed) {
-        this.visitFileFailedFunction = Objects.requireNonNull(visitFileFailed, 
"visitFileFailed");
+    protected SimplePathVisitor(final IOBiFunction<Path, IOException, 
FileVisitResult> visitFileFailedFunction) {
+        this.visitFileFailedFunction = 
Objects.requireNonNull(visitFileFailedFunction, "visitFileFailedFunction");
     }
 
     @Override
diff --git 
a/src/test/java/org/apache/commons/io/file/AccumulatorPathVisitorTest.java 
b/src/test/java/org/apache/commons/io/file/AccumulatorPathVisitorTest.java
index 45b5513e9..9a019f211 100644
--- a/src/test/java/org/apache/commons/io/file/AccumulatorPathVisitorTest.java
+++ b/src/test/java/org/apache/commons/io/file/AccumulatorPathVisitorTest.java
@@ -73,7 +73,7 @@ static Stream<Arguments> testParametersIgnoreFailures() {
         return Stream.of(
             Arguments.of((Supplier<AccumulatorPathVisitor>) () -> new 
AccumulatorPathVisitor(
                 Counters.bigIntegerPathCounters(),
-                CountingPathVisitor.defaultDirFilter(),
+                CountingPathVisitor.defaultDirectoryFilter(),
                 CountingPathVisitor.defaultFileFilter())));
         // @formatter:on
     }
diff --git 
a/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java 
b/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
index baa99cb09..45be9d739 100644
--- a/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
+++ b/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java
@@ -29,7 +29,6 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -106,13 +105,12 @@ public void testDirectoryAndFileContentEquals() throws 
Exception {
      *
      * @throws Exception on test failure.
      */
-    @Disabled
     @Test
     public void testDirectoryAndFileContentEqualsDifferentFileSystems() throws 
Exception {
         final Path dir1 = Paths.get("src/test/resources/dir-equals-tests");
         try (FileSystem fileSystem = 
FileSystems.newFileSystem(dir1.resolveSibling(dir1.getFileName() + ".zip"), 
null)) {
             final Path dir2 = fileSystem.getPath("/dir-equals-tests");
-            // WindowsPath and ZipPath equals() methods always return false if 
the argument is not of the same instance as itself.
+            // WindowsPath, UnixPath, and ZipPath equals() methods always 
return false if the argument is not of the same instance as itself.
             assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2));
         }
     }

Reply via email to