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 4e45c01df Add PathUtils.contentEquals(FileSystem, FileSystem) 4e45c01df is described below commit 4e45c01df64f9f4df04657f5037e98cde96442f8 Author: Gary D. Gregory <garydgreg...@gmail.com> AuthorDate: Sat Apr 5 11:53:56 2025 -0400 Add PathUtils.contentEquals(FileSystem, FileSystem) --- src/changes/changes.xml | 1 + .../java/org/apache/commons/io/file/PathUtils.java | 61 +++++++- .../io/file/PathUtilsContentEqualsTest.java | 164 ++++++++++++++------- 3 files changed, 169 insertions(+), 57 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index b94b1676f..0784b5533 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -93,6 +93,7 @@ The <action> type attribute can be add,update,fix,remove. <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> + <action dev="ggregory" type="add" due-to="Gary Gregory">Add PathUtils.contentEquals(FileSystem, FileSystem).</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/file/PathUtils.java b/src/main/java/org/apache/commons/io/file/PathUtils.java index 0a7e1a5f1..e67ed7f4c 100644 --- a/src/main/java/org/apache/commons/io/file/PathUtils.java +++ b/src/main/java/org/apache/commons/io/file/PathUtils.java @@ -70,6 +70,7 @@ import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.apache.commons.io.Charsets; import org.apache.commons.io.FileUtils; @@ -654,7 +655,7 @@ public static void deleteOnExit(final Path path) { } /** - * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all + * Compares the files of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all * subdirectories. * * @param path1 The first directory. @@ -666,6 +667,54 @@ public static boolean directoryAndFileContentEquals(final Path path1, final Path return directoryAndFileContentEquals(path1, path2, EMPTY_LINK_OPTION_ARRAY, EMPTY_OPEN_OPTION_ARRAY, EMPTY_FILE_VISIT_OPTION_ARRAY); } + /** + * Compares the files of two FileSystems to determine if they are equal or not while considering file contents. The comparison includes all files in all + * subdirectories. + * <p> + * For example, to compare two ZIP files: + * </p> + * + * <pre> + * final Path zipPath1 = Paths.get("file1.zip"); + * final Path zipPath2 = Paths.get("file2.zip"); + * try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath1, null); FileSystem fileSystem2 = FileSystems.newFileSystem(zipPath2, null)) { + * assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2)); + * } + * </pre> + * + * @param fileSystem1 The first FileSystem. + * @param fileSystem2 The second FileSystem. + * @return Whether the two FileSystem contain the same files while considering file contents. + * @throws IOException if an I/O error is thrown by a visitor method. + * @since 2.19.0 + */ + public static boolean contentEquals(final FileSystem fileSystem1, final FileSystem fileSystem2) throws IOException { + if (Objects.equals(fileSystem1, fileSystem2)) { + return true; + } + final List<Path> sortedList1 = toSortedList(fileSystem1.getRootDirectories()); + final List<Path> sortedList2 = toSortedList(fileSystem2.getRootDirectories()); + if (sortedList1.size() != sortedList2.size()) { + return false; + } + for (int i = 0; i < sortedList1.size(); i++) { + if (!directoryAndFileContentEquals(sortedList1.get(i), sortedList2.get(i))) { + return false; + } + } + return true; + } + + private static List<Path> toSortedList(final Iterable<Path> rootDirectories) { + final List<Path> list = toList(rootDirectories); + list.sort(Comparator.comparing(Function.identity())); + return list; + } + + private static <T> List<T> toList(final Iterable<T> iterable) { + return StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); + } + /** * Compares the file sets of two Paths to determine if they are equal or not while considering file contents. The comparison includes all files in all * subdirectories. @@ -756,8 +805,8 @@ private static boolean exists(final Path path, final LinkOption... options) { * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}. * </p> * - * @param path1 the first stream. - * @param path2 the second stream. + * @param path1 the first file path. + * @param path2 the second file path. * @return true if the content of the streams are equal or they both don't exist, false otherwise. * @throws NullPointerException if either input is null. * @throws IOException if an I/O error occurs. @@ -773,8 +822,8 @@ public static boolean fileContentEquals(final Path path1, final Path path2) thro * File content is accessed through {@link RandomAccessFileMode#create(Path)}. * </p> * - * @param path1 the first stream. - * @param path2 the second stream. + * @param path1 the first file path. + * @param path2 the second file path. * @param linkOptions options specifying how files are followed. * @param openOptions ignored. * @return true if the content of the streams are equal or they both don't exist, false otherwise. @@ -813,7 +862,7 @@ public static boolean fileContentEquals(final Path path1, final Path path2, fina // lengths differ, cannot be equal return false; } - if (path1.equals(path2)) { + if (isSameFileSystem(path1, path2) && path1.equals(path2)) { // same file return true; } 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 6b008cac3..4bae7bfd4 100644 --- a/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java +++ b/src/test/java/org/apache/commons/io/file/PathUtilsContentEqualsTest.java @@ -30,74 +30,150 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import org.apache.commons.io.file.Counters.PathCounters; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; /** * Tests {@link PathUtils}. */ public class PathUtilsContentEqualsTest { + static Configuration[] testContentEqualsFileSystemsMemVsZip() { + // @formatter:off + return new Configuration[] { + Configuration.osX().toBuilder().setWorkingDirectory("/").build(), + Configuration.unix().toBuilder().setWorkingDirectory("/").build(), + Configuration.windows().toBuilder().setWorkingDirectory("C:\\").build() + }; + // @formatter:on + } + @TempDir public File temporaryFolder; + private void assertContentEquals(final FileSystem fileSystem1, final FileSystem fileSystem2) throws IOException { + assertTrue(PathUtils.contentEquals(fileSystem1, fileSystem2)); + assertTrue(PathUtils.contentEquals(fileSystem2, fileSystem1)); + assertTrue(PathUtils.contentEquals(fileSystem1, fileSystem1)); + assertTrue(PathUtils.contentEquals(fileSystem2, fileSystem2)); + } + + private void assertContentNotEquals(final FileSystem fileSystem1, final FileSystem fileSystem2) throws IOException { + assertFalse(PathUtils.contentEquals(fileSystem1, fileSystem2)); + assertFalse(PathUtils.contentEquals(fileSystem2, fileSystem1)); + assertTrue(PathUtils.contentEquals(fileSystem1, fileSystem1)); + assertTrue(PathUtils.contentEquals(fileSystem2, fileSystem2)); + } + + private void assertDirectoryAndFileContentEquals(final Path dir1, final Path dir2) throws IOException { + assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2)); + assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1)); + assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + } + + private void assertDirectoryAndFileContentNotEquals(final Path dir1, final Path dir2) throws IOException { + assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2)); + assertFalse(PathUtils.directoryAndFileContentEquals(dir2, dir1)); + assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1)); + assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + } + + private void assertFileContentEquals(final Path path1, final Path path2) throws IOException { + assertTrue(PathUtils.fileContentEquals(path1, path1)); + assertTrue(PathUtils.fileContentEquals(path1, path2)); + assertTrue(PathUtils.fileContentEquals(path2, path2)); + assertTrue(PathUtils.fileContentEquals(path2, path1)); + } + + private void assertFileContentNotEquals(final Path path1, final Path path2) throws IOException { + assertFalse(PathUtils.fileContentEquals(path1, path2)); + assertFalse(PathUtils.fileContentEquals(path2, path1)); + assertTrue(PathUtils.fileContentEquals(path1, path1)); + assertTrue(PathUtils.fileContentEquals(path2, path2)); + } + private String getName() { return this.getClass().getSimpleName(); } + @ParameterizedTest + @MethodSource + public void testContentEqualsFileSystemsMemVsZip(final Configuration configuration) throws Exception { + final Path dir1 = Paths.get("src/test/resources/dir-equals-tests"); + try (FileSystem fileSystem1 = Jimfs.newFileSystem(configuration); + FileSystem fileSystem2 = FileSystems.newFileSystem(dir1.resolveSibling(dir1.getFileName() + ".zip"), null)) { + final Path dir2 = fileSystem1.getPath(dir1.getFileName().toString()); + final PathCounters copyDirectory = PathUtils.copyDirectory(dir1, dir2); + assertTrue(copyDirectory.getByteCounter().get() > 0); + assertContentEquals(fileSystem1, fileSystem2); + } + } + + @Test + public void testContentEqualsFileSystemsZipVsZip() throws Exception { + final Path zipPath = Paths.get("src/test/resources/dir-equals-tests.zip"); + final Path zipCopy = temporaryFolder.toPath().resolve("copy2.zip"); + Files.copy(zipPath, zipCopy, StandardCopyOption.REPLACE_EXISTING); + try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath, null); FileSystem fileSystem2 = FileSystems.newFileSystem(zipCopy, null)) { + assertContentEquals(fileSystem1, fileSystem2); + } + final Path emptyZip = Paths.get("src/test/resources/org/apache/commons/io/empty.zip"); + try (FileSystem fileSystem1 = FileSystems.newFileSystem(emptyZip, null); FileSystem fileSystem2 = FileSystems.newFileSystem(emptyZip, null)) { + assertContentEquals(fileSystem1, fileSystem2); + } + try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipCopy, null); FileSystem fileSystem2 = FileSystems.newFileSystem(emptyZip, null)) { + assertContentNotEquals(fileSystem1, fileSystem2); + } + try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath, null); FileSystem fileSystem2 = FileSystems.newFileSystem(emptyZip, null)) { + assertContentNotEquals(fileSystem1, fileSystem2); + } + } + @Test public void testDirectoryAndFileContentEquals() throws Exception { // Non-existent files final Path path1 = new File(temporaryFolder, getName()).toPath(); final Path path2 = new File(temporaryFolder, getName() + "2").toPath(); - assertTrue(PathUtils.directoryAndFileContentEquals(null, null)); - assertFalse(PathUtils.directoryAndFileContentEquals(null, path1)); - assertFalse(PathUtils.directoryAndFileContentEquals(path1, null)); + assertDirectoryAndFileContentEquals(null, null); + assertDirectoryAndFileContentNotEquals(path1, null); // both don't exist - assertTrue(PathUtils.directoryAndFileContentEquals(path1, path1)); - assertTrue(PathUtils.directoryAndFileContentEquals(path1, path2)); - assertTrue(PathUtils.directoryAndFileContentEquals(path2, path2)); - assertTrue(PathUtils.directoryAndFileContentEquals(path2, path1)); + assertDirectoryAndFileContentEquals(path1, path2); // Tree equals true tests { // Trees of files only that contain the same files. final Path dir1 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only1"); final Path dir2 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-files-only/directory-files-only2"); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + assertDirectoryAndFileContentEquals(dir1, dir2); } { // Trees of directories containing other directories. final Path dir1 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir1"); final Path dir2 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files/dir2"); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + assertDirectoryAndFileContentEquals(dir1, dir2); } { // Trees of directories containing other directories and files. final Path dir1 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1"); final Path dir2 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1"); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir1, dir1)); - assertTrue(PathUtils.directoryAndFileContentEquals(dir2, dir2)); + assertDirectoryAndFileContentEquals(dir1, dir2); } // Tree equals false tests { final Path dir1 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/directory-files-only1"); final Path dir2 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files/dirs-and-files1/"); - assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2)); - assertFalse(PathUtils.directoryAndFileContentEquals(dir2, dir1)); + assertDirectoryAndFileContentNotEquals(dir1, dir2); } { final Path dir1 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-and-files"); final Path dir2 = Paths.get("src/test/resources/dir-equals-tests/dir-equals-dirs-then-files"); - assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2)); - assertFalse(PathUtils.directoryAndFileContentEquals(dir2, dir1)); + assertDirectoryAndFileContentNotEquals(dir1, dir2); } } @@ -112,7 +188,7 @@ public void testDirectoryAndFileContentEqualsDifferentFileSystemsFileVsZip() thr try (FileSystem fileSystem = FileSystems.newFileSystem(dir1.resolveSibling(dir1.getFileName() + ".zip"), null)) { final Path dir2 = fileSystem.getPath("/dir-equals-tests"); // 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)); + assertDirectoryAndFileContentEquals(dir1, dir2); } } @@ -124,14 +200,14 @@ public void testDirectoryAndFileContentEqualsDifferentFileSystemsFileVsZip() thr @Test public void testDirectoryAndFileContentEqualsDifferentFileSystemsZipVsZip() throws Exception { final Path zipPath = Paths.get("src/test/resources/dir-equals-tests.zip"); - final Path zipCopy = temporaryFolder.toPath().resolve("copy.zip"); + final Path zipCopy = temporaryFolder.toPath().resolve("copy1.zip"); Files.copy(zipPath, zipCopy, StandardCopyOption.REPLACE_EXISTING); try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath, null); FileSystem fileSystem2 = FileSystems.newFileSystem(zipCopy, null)) { final Path dir1 = fileSystem1.getPath("/dir-equals-tests"); final Path dir2 = fileSystem2.getPath("/dir-equals-tests"); // 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)); + assertDirectoryAndFileContentEquals(dir1, dir2); } } @@ -143,30 +219,30 @@ public void testDirectoryAndFileContentEqualsDifferentFileSystemsZipVsZip() thro @Test public void testDirectoryAndFileContentEqualsDifferentFileSystemsZipVsZipEmpty() throws Exception { final Path zipPath = Paths.get("src/test/resources/dir-equals-tests.zip"); + final Path zipCopy = temporaryFolder.toPath().resolve("copy1.zip"); final Path emptyZip = Paths.get("src/test/resources/org/apache/commons/io/empty.zip"); - Files.copy(zipPath, emptyZip, StandardCopyOption.REPLACE_EXISTING); + Files.copy(zipPath, zipCopy, StandardCopyOption.REPLACE_EXISTING); try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath, null); FileSystem fileSystem2 = FileSystems.newFileSystem(emptyZip, null)) { final Path dir1 = fileSystem1.getPath("/dir-equals-tests"); final Path dir2 = fileSystem2.getPath("/"); // WindowsPath, UnixPath, and ZipPath equals() methods always return false if the argument is not of the same instance as itself. - assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2)); + assertDirectoryAndFileContentNotEquals(dir1, dir2); } try (FileSystem fileSystem1 = FileSystems.newFileSystem(zipPath, null); FileSystem fileSystem2 = FileSystems.newFileSystem(emptyZip, null)) { final Path dir1 = fileSystem1.getPath("/dir-equals-tests"); final Path dir2 = fileSystem2.getRootDirectories().iterator().next(); // WindowsPath, UnixPath, and ZipPath equals() methods always return false if the argument is not of the same instance as itself. - assertFalse(PathUtils.directoryAndFileContentEquals(dir1, dir2)); + assertDirectoryAndFileContentNotEquals(dir1, dir2); } - final Path zipCopy = temporaryFolder.toPath().resolve("copy.zip"); Files.copy(emptyZip, zipCopy, StandardCopyOption.REPLACE_EXISTING); try (FileSystem fileSystem1 = FileSystems.newFileSystem(emptyZip, null); FileSystem fileSystem2 = FileSystems.newFileSystem(zipCopy, null)) { final Path dir1 = fileSystem1.getPath("/"); final Path dir2 = fileSystem2.getPath("/"); // 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)); + assertDirectoryAndFileContentEquals(dir1, dir2); } } @@ -232,40 +308,28 @@ public void testFileContentEquals() throws Exception { final Path path1 = new File(temporaryFolder, getName()).toPath(); final Path path2 = new File(temporaryFolder, getName() + "2").toPath(); assertTrue(PathUtils.fileContentEquals(null, null)); - assertFalse(PathUtils.fileContentEquals(null, path1)); - assertFalse(PathUtils.fileContentEquals(path1, null)); + assertFileContentNotEquals(path1, null); // both don't exist - assertTrue(PathUtils.fileContentEquals(path1, path1)); - assertTrue(PathUtils.fileContentEquals(path1, path2)); - assertTrue(PathUtils.fileContentEquals(path2, path2)); - assertTrue(PathUtils.fileContentEquals(path2, path1)); - + assertFileContentEquals(path1, path2); // Directories assertThrows(IOException.class, () -> PathUtils.fileContentEquals(temporaryFolder.toPath(), temporaryFolder.toPath())); - // Different files final Path objFile1 = Paths.get(temporaryFolder.getAbsolutePath(), getName() + ".object"); PathUtils.copyFile(getClass().getResource("/java/lang/Object.class"), objFile1); - final Path objFile1b = Paths.get(temporaryFolder.getAbsolutePath(), getName() + ".object2"); PathUtils.copyFile(getClass().getResource("/java/lang/Object.class"), objFile1b); - final Path objFile2 = Paths.get(temporaryFolder.getAbsolutePath(), getName() + ".collection"); PathUtils.copyFile(getClass().getResource("/java/util/Collection.class"), objFile2); - assertFalse(PathUtils.fileContentEquals(objFile1, objFile2)); assertFalse(PathUtils.fileContentEquals(objFile1b, objFile2)); assertTrue(PathUtils.fileContentEquals(objFile1, objFile1b)); - assertTrue(PathUtils.fileContentEquals(objFile1, objFile1)); assertTrue(PathUtils.fileContentEquals(objFile1b, objFile1b)); assertTrue(PathUtils.fileContentEquals(objFile2, objFile2)); - // Equal files Files.createFile(path1); Files.createFile(path2); - assertTrue(PathUtils.fileContentEquals(path1, path1)); - assertTrue(PathUtils.fileContentEquals(path1, path2)); + assertFileContentEquals(path1, path2); } @Test @@ -274,8 +338,8 @@ public void testFileContentEqualsZip() throws Exception { final Path path2 = Paths.get("src/test/resources/org/apache/commons/io/bla-copy.zip"); // moby.zip is from https://issues.apache.org/jira/browse/COMPRESS-93 final Path path3 = Paths.get("src/test/resources/org/apache/commons/io/moby.zip"); - assertTrue(PathUtils.fileContentEquals(path1, path2)); - assertFalse(PathUtils.fileContentEquals(path1, path3)); + assertFileContentEquals(path1, path2); + assertFileContentNotEquals(path1, path3); } @Test @@ -288,9 +352,7 @@ public void testFileContentEqualsZipFileSystem() throws Exception { final Path path2 = fileSystem.getPath("/test-same-size-diff-contents/B.txt"); assertTrue(Files.exists(path1)); assertTrue(Files.exists(path2)); - assertTrue(PathUtils.fileContentEquals(path1, path1)); - assertTrue(PathUtils.fileContentEquals(path2, path2)); - assertFalse(PathUtils.fileContentEquals(path1, path2)); + assertFileContentNotEquals(path1, path2); } }