--- old/src/java.base/share/classes/java/nio/file/Files.java 2018-09-18 19:47:11.848369329 -0700 +++ new/src/java.base/share/classes/java/nio/file/Files.java 2018-09-18 19:47:10.958274145 -0700 @@ -91,6 +91,9 @@ */ public final class Files { + // buffer size used for reading and writing + static final int BUFFER_SIZE = 8192; + private Files() { } /** @@ -1530,6 +1533,53 @@ return provider(path).isSameFile(path, path2); } + + /** + * Finds and returns the index of the first mismatched byte in the content + * of two files, or returns -1 if the files are equal. + * + *

Two files are considered equal if they satisfy one of the following + * conditions: + *

+ * + *

Otherwise, there is a mismatch between the two files. This method returns + * a mismatch value, computed as follows: + *

+ * + *

This method may not be atomic with respect to other file system operations. + * + * @param path + * the path to the first file + * @param path2 + * the path to the second file + * + * @return the index of the first mismatch between the two files, or {@code -1} + * if there is no mismatch. + * + * @throws IOException + * if an I/O error occurs, or path or path2 does not exist unless + * they are the same object + * @throws SecurityException + * In the case of the default provider, and a security manager is + * installed, the {@link SecurityManager#checkRead(String) checkRead} + * method is invoked to check read access to both files. + * @since 12 + * + */ + public static long mismatch(Path path, Path path2) throws IOException { + return FilesMismatch.mismatch(path, path2); + } + /** * Tells whether or not a file is considered hidden. The exact * definition of hidden is platform or provider dependent. On UNIX for @@ -2802,8 +2852,6 @@ // -- Utility methods for simple usages -- - // buffer size used for reading and writing - private static final int BUFFER_SIZE = 8192; /** * Opens a file for reading, returning a {@code BufferedReader} that may be --- /dev/null 2018-05-07 23:51:01.519000000 -0700 +++ new/src/java.base/share/classes/java/nio/file/FilesMismatch.java 2018-09-18 19:47:12.936485688 -0700 @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package java.nio.file; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; + +/** + * Utility methods for Files.mismatch. + */ +class FilesMismatch { + // see Files.mismatch for the specification + static long mismatch(Path path, Path path2) throws IOException { + long result = mismatchByAttrs(path, path2); + if (result != -2) { + return result; + } + + try (InputStream in1 = Files.newInputStream(path); + InputStream in2 = Files.newInputStream(path2);) { + result = mismatch(in1, in2); + return result; + } + } + + // check mismatch by file attributes, return -1 for no mismatch, 0 for mismatch + // at the beginning of the file, or -2 to indicate undetermined + private static long mismatchByAttrs(Path path, Path path2) throws IOException { + // initial values: -1 -- no mismatch + long result = -1; + try { + // the following checks NPE as well + path = path.toRealPath(); + path2 = path2.toRealPath(); + if (path.compareTo(path2) == 0) { + return result; + } + // readAttributes checks whether the file exists, throws IOE if not + BasicFileAttributes attrs1 = Files.readAttributes(path, BasicFileAttributes.class); + BasicFileAttributes attrs2 = Files.readAttributes(path2, BasicFileAttributes.class); + long s1 = attrs1.size(); + long s2 = attrs2.size(); + + if (s1 == 0 && s2 == 0) { + return result; + } + if (s1 == 0 && s2 > 0 || s1 > 0 && s2 == 0) { + return 0; + } + if (s1 == s2) { + Object k1 = attrs1.fileKey(); + if (k1 != null && k1.equals(attrs2.fileKey())) { + return result; + } + } + } catch (ProviderMismatchException e) { + throw new UnsupportedOperationException(e); + } + return -2; + } + + /** + * Finds and returns the index of the first mismatching byte in the content + * of two files. + * + * @param in1 an inputstream + * @param in2 another inputstream + * @return the mismatch index if a mismatch is found, otherwise -1 to indicate + * no mismatch + * @throws IOException + */ + private static long mismatch(InputStream in1, InputStream in2) + throws IOException { + byte[] buffer1 = new byte[Files.BUFFER_SIZE]; + byte[] buffer2 = new byte[Files.BUFFER_SIZE]; + int totalRead = 0; + while (true) { + int nRead1 = in1.readNBytes(buffer1, 0, Files.BUFFER_SIZE); + int nRead2 = in2.readNBytes(buffer2, 0, Files.BUFFER_SIZE); + + if (nRead1 == 0 || nRead2 == 0) { + if (nRead1 == 0 && nRead2 == 0) { + // both files reach EOF + return -1; + } else { + // one of the files reaches EOF + return totalRead; + } + } else if (nRead1 != nRead2) { + int len = Math.min(nRead1, nRead2); + // there's always a mismatch when nRead1 != nRead2 + return totalRead + + Arrays.mismatch(buffer1, 0, len, buffer2, 0, len); + + } else { + int i = Arrays.mismatch(buffer1, 0, nRead1, buffer2, 0, nRead1); + if (i > -1) { + return totalRead + i; + } + if (nRead1 < Files.BUFFER_SIZE) { + // we've reached the end of the files, but found no mismatch + return -1; + } + totalRead += nRead1; + } + } + } +} --- /dev/null 2018-05-07 23:51:01.519000000 -0700 +++ new/test/jdk/java/nio/file/Files/Comparison.java 2018-09-18 19:47:14.754680120 -0700 @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.*; +import java.nio.channels.Channels; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.Arrays; +import java.util.Random; + +/* @test + * @bug 8202285 + * @build Comparison + * @run testng Comparison + * @summary Unit test for the mismatch method + */ +@Test(groups = "comparison") +public class Comparison { + // buffer size used for reading and writing + private static final int BUFFER_SIZE = 8192; + private static final int MISMATCH_NO = -1; + private static final int COMPARE_EQUAL = 0; + + Path[] testFiles = new Path[18]; + Path[] testDirs = new Path[2]; + + @BeforeClass + void setup() throws IOException { + int size = 0; + testFiles[0] = prepareTestFiles("compareEmptyFile1", 0, -1, ' '); + testFiles[1] = prepareTestFiles("compareEmptyFile2", 0, -1, ' '); + testFiles[2] = prepareTestFiles("compareOrdered120_1", 120, -1, ' '); + testFiles[3] = prepareTestFiles("compareOrdered120_2", 120, -1, ' '); + testFiles[4] = prepareTestFiles("compareAltered120_A", 120, 110, 'A'); + testFiles[5] = prepareTestFiles("compareAltered120_B", 120, 110, 'Z'); + testFiles[6] = prepareTestFiles("compareAltered8192_B", BUFFER_SIZE, 110, 'Z'); + testFiles[7] = prepareTestFiles("compareOrdered8192_1", BUFFER_SIZE, -1, ' '); + testFiles[8] = prepareTestFiles("compareOrdered8192_2", BUFFER_SIZE, -1, ' '); + size = BUFFER_SIZE << 1; + testFiles[9] = prepareTestFiles("compareOrdered16384_1", size, -1, ' '); + testFiles[10] = prepareTestFiles("compareOrdered16384_2", size, -1, ' '); + testFiles[11] = prepareTestFiles("compareAltered16384_A", size, BUFFER_SIZE, 'A'); + testFiles[12] = prepareTestFiles("compareAltered16384_B", size, BUFFER_SIZE, 'Z'); + size = BUFFER_SIZE << 2; + testFiles[13] = prepareTestFiles("compareAltered32768_B", size, BUFFER_SIZE, 'Z'); + testFiles[14] = prepareTestFiles("compareOrdered32768_1", size, -1, ' '); + testFiles[15] = prepareTestFiles("compareOrdered32768_2", size, -1, ' '); + testFiles[16] = prepareTestFiles("compareAltered32768_1", size, size - 1, 'A'); + testFiles[17] = prepareTestFiles("compareAltered32768_2", size, size - 1, 'Z'); + + // testing directories + testDirs[0] = Files.createTempDirectory("comparison_foo"); + testDirs[1] = Files.createTempDirectory("comparison_bar"); + } + + @AfterClass + void cleanup() throws IOException { + for (Path p : testFiles) { + Files.deleteIfExists(p); + } + for (Path p : testDirs) { + Files.deleteIfExists(p); + } + } + + /* + * DataProvider for mismatch test. Provides the following fields: + * path1, path2, expected result + */ + + @DataProvider(name = "testMismatch") + public Object[][] getDataForMismatch() throws IOException { + + return new Object[][]{ + // irregular files (directories) are not considered error (no UOE) + {testDirs[0], testDirs[0], MISMATCH_NO, "Both are directory, but they are the same"}, + // edge cases + {testFiles[0], testFiles[0], MISMATCH_NO, "Both files are empty, no mismatch"}, + {testFiles[2], testFiles[0], 0, "Compares with an empty file, mismatch at 0"}, + // same size + {testFiles[2], testFiles[2], MISMATCH_NO, "Same file, no mismatch"}, + {testFiles[2], testFiles[3], MISMATCH_NO, "Same content, no mismatch"}, + {testFiles[4], testFiles[5], 110, "Mismatch at 110"}, + {testFiles[5], testFiles[4], 110, "Mismatch at 110"}, + {testFiles[7], testFiles[8], MISMATCH_NO, "Same content, no mismatch"}, + {testFiles[11], testFiles[12], BUFFER_SIZE, "Mismatch at BUFFER_SIZE"}, + {testFiles[14], testFiles[15], MISMATCH_NO, "Same content, no mismatch"}, + {testFiles[16], testFiles[17], (BUFFER_SIZE << 2) - 1, "Mismatch at size - 1"}, + // different size + {testFiles[4], testFiles[6], 110, "Mismatch at 110"}, + {testFiles[8], testFiles[9], BUFFER_SIZE, "Mismatch = length of the smaller file"}, + {testFiles[7], testFiles[14], BUFFER_SIZE, "Mismatch = length of the smaller file"}, + {testFiles[11], testFiles[13], BUFFER_SIZE, "Mismatch at BUFFER_SIZE"}, + }; + } + + @DataProvider(name = "testFileNull") + public Object[][] getDataForNull() throws IOException { + return new Object[][]{ + {(Path)null, (Path)null}, + {(Path)null, testFiles[0]}, + {testFiles[0], (Path)null}, + }; + } + + @DataProvider(name = "testFileNotExist") + public Object[][] getDataForFileNotExist() throws IOException { + return new Object[][]{ + {Paths.get("foo"), Paths.get("foo")}, + {Paths.get("foo"), Paths.get("bar")}, + {Paths.get("foo"), testFiles[0]}, + {testFiles[0], Paths.get("bar")}, + }; + } + + @Test(dataProvider = "testMismatch", priority = 0) + public void testMismatch(Path path, Path path2, long expected, String msg) + throws IOException { + //System.out.println("mismatch " + path + " with " + path2); + long result = Files.mismatch(path, path2); + //System.out.println("Result " + result + " expected " + expected); + Assert.assertTrue(result == expected, msg); + } + + @Test(dataProvider = "testFileNull", priority = 2, expectedExceptions = NullPointerException.class) + public void testMismatchNull(Path path, Path path2) throws IOException { + long result = Files.mismatch(path, path2); + } + + @Test(dataProvider = "testFileNotExist", priority = 2, expectedExceptions = IOException.class) + public void testMismatchNotExist(Path path, Path path2) throws IOException { + long result = Files.mismatch(path, path2); + } + + // Prepares a test file + private Path prepareTestFiles(String purpose, int size, int pos, char c) throws IOException { + Path path = Files.createTempFile(purpose, "txt"); + if (size > 0) { + writeFile(path, size, pos, c, null); + } + return path; + } + + private void writeFile(Path file, int size, int pos, char c, Charset cs) throws IOException { + String expected; + String str = generateFixedString(size); + if (pos > -1) { + str = insertChar(str, pos, c); + } + Path result; + if (cs == null) { + result = Files.writeString(file, str, StandardOpenOption.CREATE); + } else { + result = Files.writeString(file, str, cs, StandardOpenOption.CREATE); + } + } + + /** + * Inserts a char after the specified position (index). + * @param str the original string + * @param pos the position after which the char will be inserted + * @param c a char + */ + private String insertChar(String str, int pos, char c) { + String temp = str.substring(0, pos) + c; + if (pos + 1 < str.length()) { + temp += str.substring(pos + 1); + } + //System.out.println(temp); + return temp; + } + + static final String STR = "abcdefghijklmnopqrstuvwxyz \r\n"; + StringBuilder sb = new StringBuilder(1024 << 4); + + private String generateFixedString(int size) { + sb.setLength(0); + int total = 0; + int len = STR.length(); + int x = size - total; + while (x > 0) { + x = size - total; + if (x >= len) { + sb.append(STR); + total += len; + } else { + if (x > 0) { + sb.append(STR.substring(0, x)); + } + break; + } + } + + return sb.toString(); + } +}