Skip to content

Commit 5e5178b

Browse files
committed
Ensure exception on chunk reaching beyond limit
This is a subtle behavior, but previous behavior has been that the first invocation to read(byte[]) with an array that would be filled with more elements if not being limited, would actually throw an exception. This commit preserves this behavior. One might argue that the most beneficial way to handle reading the "last chunk" before reaching the limit would be to fill the the array with any remaining "allowed" bytes, and return an int indicating the amount of bytes propagated to the array, though this also indicates that the stream reached a normal last chunk of bytes, and one can assume next read would return -1 (EOF). When setting up the LimitedInputStream to throw an exception if reading past the limit, this will ensure that code which assume that they can read one chunk of bytes, and as long as the read succeeds, they should be given complete and well-formed data. This would not be the case if the LimitedInputStream limits the bytes, and if it is set up to throw an exception, this exception must be thrown. If the LimitedInputStream is set up to "silently" yield EOF on reaching the limit, the read chunk is correctly chopped accordinly, and the next read will yield EOF as expected.
1 parent ecb3a8e commit 5e5178b

File tree

2 files changed

+88
-6
lines changed

2 files changed

+88
-6
lines changed

src/main/java/no/digipost/io/LimitedInputStream.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.FilterInputStream;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.util.Arrays;
2223
import java.util.function.Supplier;
2324

2425
import static java.lang.Math.max;
@@ -138,10 +139,19 @@ public int read() throws IOException {
138139
@Override
139140
public int read(byte[] b, int off, int len) throws IOException {
140141
int allowedRemaing = (int)(maxBytesCount - count);
142+
if (len == 0) {
143+
return allowedRemaing > 0 ? 0 : -1;
144+
}
141145
int res;
142146
if (allowedRemaing > 0) {
143-
res = super.read(b, off, min(len, allowedRemaing));
147+
int maxAllowedReadLen = min(len, allowedRemaing + 1);
148+
res = super.read(b, off, maxAllowedReadLen);
144149
count += max(res, 1);
150+
if (res > allowedRemaing) {
151+
Arrays.fill(b, off + maxAllowedReadLen - (res - allowedRemaing), off + maxAllowedReadLen, (byte)0);
152+
res = allowedRemaing;
153+
reachedLimit();
154+
}
145155
} else {
146156
res = read();
147157
}

src/test/java/no/digipost/io/LimitedInputStreamTest.java

+77-5
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,39 @@ void neverReadsMoreThanTheSetLimit() {
8787
}
8888
});
8989
}
90+
91+
@Test
92+
void discardsBytesAfterLimit() throws IOException {
93+
byte[] sixBytes = new byte[] {65, 66, 67, 68, 69, 70};
94+
try (
95+
InputStream source = new ByteArrayInputStream(sixBytes);
96+
InputStream limitedToTwoBytes = limit(source, bytes(4))) {
97+
98+
99+
byte[] readBytes = new byte[6];
100+
byte[] expectedEmpty = new byte[2];
101+
assertAll(
102+
() -> assertThat("first read yields 4 bytes", limitedToTwoBytes.read(readBytes), is(4)),
103+
() -> assertArrayEquals(new byte[] {65, 66, 67, 68, 0, 0}, readBytes),
104+
() -> assertThat("reading single read yields EOF", limitedToTwoBytes.read(), is(-1)),
105+
() -> assertThat("reading chunk yields EOF", limitedToTwoBytes.read(expectedEmpty), is(-1)),
106+
() -> assertArrayEquals(new byte[] {0, 0}, expectedEmpty));
107+
}
108+
}
109+
110+
@Test
111+
void readingZeroBytesYieldsZeroOrEof() throws IOException {
112+
byte[] twoBytes = new byte[] {65, 66};
113+
try (
114+
InputStream source = new ByteArrayInputStream(twoBytes);
115+
InputStream limitedToOneByte = limit(source, bytes(1))) {
116+
assertThat(limitedToOneByte.read(new byte[0]), is(0));
117+
limitedToOneByte.read();
118+
assertThat(limitedToOneByte.read(new byte[0]), is(-1));
119+
assertThat(limitedToOneByte.read(), is(-1));
120+
}
121+
}
122+
90123
}
91124

92125

@@ -117,6 +150,45 @@ public void wrapsOtherCheckedExceptionsThanIOExceptionAsRuntimeException() throw
117150
assertThat(assertThrows(RuntimeException.class, () -> testLimitedStream("xyz", () -> tooManyBytes)), where(Exception::getCause, sameInstance(tooManyBytes)));
118151
}
119152

153+
@Test
154+
void throwsWhenAttemptingToReadChunkOneByteLargerThanLimit() throws IOException {
155+
byte[] sixBytes = new byte[] {65, 66, 67, 68, 69, 70};
156+
try (
157+
InputStream source = new ByteArrayInputStream(sixBytes);
158+
InputStream limitedToTwoBytes = limit(source, bytes(5), () -> new IllegalStateException("reached limit!"))) {
159+
160+
assertThrows(IllegalStateException.class, () -> limitedToTwoBytes.read(new byte[6]));
161+
}
162+
}
163+
164+
@Test
165+
void doesNotThrowWhenAttemptingToReadChunkExactlyEndingAtTheLimit() throws IOException {
166+
byte[] sixBytes = new byte[] {65, 66, 67, 68, 69, 70};
167+
try (
168+
InputStream source = new ByteArrayInputStream(sixBytes);
169+
InputStream limitedToTwoBytes = limit(source, bytes(6), () -> new IllegalStateException("reached limit!"))) {
170+
171+
limitedToTwoBytes.read(new byte[3]);
172+
byte[] lastThreeBytes = new byte[3];
173+
limitedToTwoBytes.read(lastThreeBytes);
174+
assertArrayEquals(new byte[]{68, 69, 70}, lastThreeBytes);
175+
}
176+
}
177+
178+
@Test
179+
void readingZeroBytesNeverThrowsUntilTryingToActuallyReadNonZeroBytes() throws IOException {
180+
byte[] twoBytes = new byte[] {65, 66};
181+
try (
182+
InputStream source = new ByteArrayInputStream(twoBytes);
183+
InputStream limitedToOneByte = limit(source, bytes(1), () -> new IOException("Should not be thrown"))) {
184+
assertThat(limitedToOneByte.read(new byte[0]), is(0));
185+
limitedToOneByte.read();
186+
assertThat(limitedToOneByte.read(new byte[0]), is(-1));
187+
assertThrows(IOException.class, () -> limitedToOneByte.read());
188+
assertThrows(IOException.class, () -> limitedToOneByte.read(new byte[1]));
189+
}
190+
}
191+
120192
}
121193

122194

@@ -181,9 +253,10 @@ void doesNotConsumeMoreFromUnderlyingInputStreamThanGivenLimit() throws IOExcept
181253
InputStream threeBytes = new ByteArrayInputStream(new byte[] {65, 66, 67});
182254
InputStream maxTwoBytes = limit(threeBytes, bytes(2))) {
183255

184-
assertThat(maxTwoBytes.read(readBytes), is(2));
256+
assertAll(
257+
() -> assertThat(maxTwoBytes.read(readBytes), is(2)),
258+
() -> assertArrayEquals(new byte[] {65, 66, 0}, readBytes));
185259
}
186-
assertArrayEquals(new byte[] {65, 66, 0}, readBytes);
187260
}
188261

189262
@Test
@@ -198,8 +271,8 @@ void ableToResetBufferedStreamWhenLimitedStreamIsExhausted() throws IOException
198271
InputStream source = new ByteArrayInputStream(oneKiloByte);
199272
InputStream bufferedSource = new BufferedInputStream(source, 400)) {
200273

201-
bufferedSource.mark(limit);
202-
try (InputStream limitedStream = limit(bufferedSource, DataSize.bytes(limit), () -> new IllegalStateException("Reached limit!"))) {
274+
bufferedSource.mark(limit + 1); // <-- :(
275+
try (InputStream limitedStream = limit(bufferedSource, DataSize.bytes(limit))) {
203276
assertThat(limitedStream.read(readFromLimitedStream), is(limit));
204277
bufferedSource.reset();
205278
bufferedSource.read(readFromBufferedStream);
@@ -234,7 +307,6 @@ void rewindWhenReachingLimit() throws IOException {
234307
assertArrayEquals(readFromBufferedStream, twoKiloByte);
235308
}
236309
}
237-
238310
}
239311

240312
}

0 commit comments

Comments
 (0)