Skip to content

Commit dde1d39

Browse files
committed
[IO-871] IOUtils.contentEquals is incorrect when InputStream.available
under-reports
1 parent c2e18c6 commit dde1d39

File tree

4 files changed

+89
-66
lines changed

4 files changed

+89
-66
lines changed

src/main/java/org/apache/commons/io/channels/FileChannels.java

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -60,78 +60,43 @@ public static boolean contentEquals(final FileChannel channel1, final FileChanne
6060
*/
6161
public static boolean contentEquals(final ReadableByteChannel channel1, final ReadableByteChannel channel2, final int bufferCapacity) throws IOException {
6262
// Before making any changes, please test with org.apache.commons.io.jmh.IOUtilsContentEqualsInputStreamsBenchmark
63-
if (bufferCapacity <= 0) {
64-
throw new IllegalArgumentException();
65-
}
6663
// Short-circuit test
6764
if (Objects.equals(channel1, channel2)) {
6865
return true;
6966
}
7067
// Dig in and do the work
7168
final ByteBuffer c1Buffer = ByteBuffer.allocateDirect(bufferCapacity);
7269
final ByteBuffer c2Buffer = ByteBuffer.allocateDirect(bufferCapacity);
73-
int c1ReadNum = -1;
74-
int c2ReadNum = -1;
75-
boolean c1Read = true;
76-
boolean c2Read = true;
77-
boolean equals = false;
70+
int c1NumRead = 0;
71+
int c2NumRead = 0;
72+
boolean c1Read0 = false;
73+
boolean c2Read0 = false;
7874
// If a channel is a non-blocking channel, it may return 0 bytes read for any given call.
7975
while (true) {
80-
// don't call compact() in this method to avoid copying
81-
if (c1Read) {
82-
c1ReadNum = channel1.read(c1Buffer);
83-
c1Buffer.position(0);
84-
c1Read = c1ReadNum >= 0;
76+
if (!c2Read0) {
77+
c1NumRead = readToLimit(channel1, c1Buffer);
78+
c1Buffer.clear();
79+
c1Read0 = c1NumRead == 0;
8580
}
86-
if (c2Read) {
87-
c2ReadNum = channel2.read(c2Buffer);
88-
c2Buffer.position(0);
89-
c2Read = c2ReadNum >= 0;
81+
if (!c1Read0) {
82+
c2NumRead = readToLimit(channel2, c2Buffer);
83+
c2Buffer.clear();
84+
c2Read0 = c2NumRead == 0;
9085
}
91-
if (c1ReadNum == IOUtils.EOF && c2ReadNum == IOUtils.EOF) {
92-
return equals || c1Buffer.equals(c2Buffer);
86+
if (c1NumRead == IOUtils.EOF && c2NumRead == IOUtils.EOF) {
87+
return c1Buffer.equals(c2Buffer);
9388
}
94-
if (c1ReadNum == 0 || c2ReadNum == 0) {
89+
if (c1NumRead == 0 || c2NumRead == 0) {
90+
// 0 may be returned from a non-blocking channel.
9591
Thread.yield();
96-
}
97-
if (c1ReadNum == 0 && c2ReadNum == IOUtils.EOF || c2ReadNum == 0 && c1ReadNum == IOUtils.EOF) {
9892
continue;
9993
}
100-
if (c1ReadNum != c2ReadNum) {
101-
final int limit = Math.min(c1ReadNum, c2ReadNum);
102-
if (limit == IOUtils.EOF) {
103-
return false;
104-
}
105-
c1Buffer.limit(limit);
106-
c2Buffer.limit(limit);
107-
if (!c1Buffer.equals(c2Buffer)) {
108-
return false;
109-
}
110-
equals = true;
111-
c1Buffer.limit(bufferCapacity);
112-
c2Buffer.limit(bufferCapacity);
113-
c1Read = c2ReadNum > c1ReadNum;
114-
c2Read = c1ReadNum > c2ReadNum;
115-
if (c1Read) {
116-
c1Buffer.position(0);
117-
} else {
118-
c1Buffer.position(limit);
119-
c2Buffer.limit(c1Buffer.remaining());
120-
c1ReadNum -= c2ReadNum;
121-
}
122-
if (c2Read) {
123-
c2Buffer.position(0);
124-
} else {
125-
c2Buffer.position(limit);
126-
c1Buffer.limit(c2Buffer.remaining());
127-
c2ReadNum -= c1ReadNum;
128-
}
129-
continue;
94+
if (c1NumRead != c2NumRead) {
95+
return false;
13096
}
13197
if (!c1Buffer.equals(c2Buffer)) {
13298
return false;
13399
}
134-
equals = c1Read = c2Read = true;
135100
}
136101
}
137102

@@ -162,6 +127,36 @@ public static boolean contentEquals(final SeekableByteChannel channel1, final Se
162127
return size1 == 0 && size2 == 0 || contentEquals((ReadableByteChannel) channel1, channel2, bufferCapacity);
163128
}
164129

130+
/**
131+
* Reads a sequence of bytes from a channel into the given buffer until the buffer reaches its limit or the channel has reaches end-of-stream.
132+
* <p>
133+
* The buffer's limit is not changed.
134+
* </p>
135+
*
136+
* @param channel The source channel.
137+
* @param dst The buffer into which bytes are to be transferred.
138+
* @return The number of bytes read, possibly zero, or {@code-1} if the channel has reached end-of-stream
139+
* @throws IOException If some other I/O error occurs.
140+
* @throws IllegalArgumentException If there is room in the given buffer.
141+
*/
142+
private static int readToLimit(final ReadableByteChannel channel, final ByteBuffer dst) throws IOException {
143+
if (!dst.hasRemaining()) {
144+
throw new IllegalArgumentException();
145+
}
146+
int numRead = 0;
147+
int totalRead = 0;
148+
while (dst.hasRemaining()) {
149+
if ((totalRead += numRead = channel.read(dst)) == IOUtils.EOF) {
150+
break;
151+
}
152+
if (numRead == 0) {
153+
// 0 may be returned from a non-blocking channel.
154+
Thread.yield();
155+
}
156+
}
157+
return totalRead;
158+
}
159+
165160
private static long size(final SeekableByteChannel channel) throws IOException {
166161
return channel != null ? channel.size() : 0;
167162
}

src/test/java/org/apache/commons/io/IOUtilsTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.nio.file.Files;
6262
import java.nio.file.Path;
6363
import java.util.Arrays;
64+
import java.util.Collections;
6465
import java.util.List;
6566
import java.util.concurrent.atomic.AtomicBoolean;
6667
import java.util.function.Consumer;
@@ -692,6 +693,7 @@ public void testContentEqualsIgnoreEOL() throws Exception {
692693

693694
@Test
694695
public void testContentEqualsSequenceInputStream() throws Exception {
696+
// https://issues.apache.org/jira/browse/IO-866
695697
// not equals
696698
// @formatter:off
697699
assertFalse(IOUtils.contentEquals(
@@ -746,6 +748,24 @@ public void testContentEqualsSequenceInputStream() throws Exception {
746748
new ByteArrayInputStream("".getBytes()),
747749
new ByteArrayInputStream("ab".getBytes()))));
748750
// @formatter:on
751+
final byte[] prefixLen32 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2 };
752+
final byte[] suffixLen2 = { 1, 2 };
753+
final byte[] fileContents = "someTexts".getBytes(StandardCharsets.UTF_8);
754+
Files.write(testFile.toPath(), fileContents);
755+
final byte[] expected = new byte[prefixLen32.length + fileContents.length + suffixLen2.length];
756+
System.arraycopy(prefixLen32, 0, expected, 0, prefixLen32.length);
757+
System.arraycopy(fileContents, 0, expected, prefixLen32.length, fileContents.length);
758+
System.arraycopy(suffixLen2, 0, expected, prefixLen32.length + fileContents.length, suffixLen2.length);
759+
// @formatter:off
760+
assertTrue(IOUtils.contentEquals(
761+
new ByteArrayInputStream(expected),
762+
new SequenceInputStream(
763+
Collections.enumeration(
764+
Arrays.asList(
765+
new ByteArrayInputStream(prefixLen32),
766+
new FileInputStream(testFile),
767+
new ByteArrayInputStream(suffixLen2))))));
768+
// @formatter:on
749769
}
750770

751771
@Test

src/test/java/org/apache/commons/io/channels/FileChannelsTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ private void testContentEquals(final String content1, final String content2, fin
114114
FileUtils.writeStringToFile(file2, content2, US_ASCII);
115115
// File checksums are different
116116
assertNotEquals(FileUtils.checksumCRC32(file1), FileUtils.checksumCRC32(file2));
117+
// content not equals
117118
try (FileInputStream in1 = new FileInputStream(file1);
118119
FileInputStream in2 = new FileInputStream(file2);
119120
FileChannel channel1 = getChannel(in1, fileChannelType, bufferSize);
120121
FileChannel channel2 = getChannel(in2, fileChannelType, half(bufferSize))) {
121122
assertFalse(FileChannels.contentEquals(channel1, channel2, bufferSize));
122123
}
124+
// content not equals
123125
try (FileInputStream in1 = new FileInputStream(file1);
124126
FileInputStream in2 = new FileInputStream(file2);
125127
FileChannel channel1 = getChannel(in1, fileChannelType, bufferSize);

src/test/java/org/apache/commons/io/channels/FixedReadSizeFileChannelProxy.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.nio.channels.FileChannel;
2323

2424
/**
25-
* Always reads the same amount of bytes on each call or less.
25+
* Always reads the same amount of bytes on each call (or less).
2626
*/
2727
class FixedReadSizeFileChannelProxy extends FileChannelProxy {
2828

@@ -38,26 +38,32 @@ class FixedReadSizeFileChannelProxy extends FileChannelProxy {
3838

3939
@Override
4040
public int read(final ByteBuffer dst) throws IOException {
41-
final int limit = dst.limit();
42-
dst.limit(Math.min(readSize, dst.limit()));
43-
final int read = super.read(dst);
44-
if (read > readSize) {
41+
final int saveLimit = dst.limit();
42+
dst.limit(Math.min(dst.position() + readSize, dst.capacity()));
43+
if (!dst.hasRemaining()) {
4544
throw new IllegalStateException("Programming error.");
4645
}
47-
dst.limit(limit);
48-
return read;
46+
final int numRead = super.read(dst);
47+
if (numRead > readSize) {
48+
throw new IllegalStateException(String.format("numRead %,d > readSize %,d", numRead, readSize));
49+
}
50+
dst.limit(saveLimit);
51+
return numRead;
4952
}
5053

5154
@Override
5255
public int read(final ByteBuffer dst, final long position) throws IOException {
53-
final int limit = dst.limit();
54-
dst.limit(Math.min(readSize, dst.limit()));
55-
final int read = super.read(dst, position);
56-
if (read > readSize) {
56+
final int saveLimit = dst.limit();
57+
dst.limit(Math.min(dst.position() + readSize, dst.capacity()));
58+
if (!dst.hasRemaining()) {
5759
throw new IllegalStateException("Programming error.");
5860
}
59-
dst.limit(limit);
60-
return read;
61+
final int numRead = super.read(dst, position);
62+
if (numRead > readSize) {
63+
throw new IllegalStateException(String.format("numRead %,d > readSize %,d", numRead, readSize));
64+
}
65+
dst.limit(saveLimit);
66+
return numRead;
6167
}
6268

6369
@Override

0 commit comments

Comments
 (0)