From 14639d2575b32fbdc93aa84a58b1f312a7ccbdf8 Mon Sep 17 00:00:00 2001
From: Andreas Loth
Date: Thu, 12 Oct 2023 15:20:19 +0200
Subject: [PATCH 1/4] [IO-427] Add TrailerInputStream
---
.../commons/io/input/TrailerInputStream.java | 180 ++++++++++++
.../io/input/TrailerInputStreamTest.java | 263 ++++++++++++++++++
2 files changed, 443 insertions(+)
create mode 100644 src/main/java/org/apache/commons/io/input/TrailerInputStream.java
create mode 100644 src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
diff --git a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
new file mode 100644
index 00000000000..4c86cb5903d
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Reads the underlying input stream while holding back the trailer.
+ *
+ *
+ * "Normal" read calls read the underlying stream except the last few bytes (the trailer). The
+ * trailer is updated with each read call. The trailer can be gotten by one of the copyTrailer
+ * overloads.
+ *
+ *
+ *
+ * It is safe to fetch the trailer at any time but the trailer will change with each read call
+ * until the underlying stream is EOF.
+ *
+ *
+ *
+ * Useful, e.g., for handling checksums: payload is followed by a fixed size hash, so while
+ * streaming the payload the trailer finally contains the expected hash (this example needs
+ * extra caution to revert actions when the final checksum match fails).
+ *
+ */
+public final class TrailerInputStream extends InputStream {
+
+ private final InputStream source;
+ /**
+ * Invariant: After every method call which exited without exception, the trailer has to be
+ * completely filled.
+ */
+ private final byte[] trailer;
+
+ /**
+ * Constructs the TrailerInputStream and initializes the trailer buffer.
+ *
+ *
+ * Reads exactly {@code trailerLength} bytes from {@code source}.
+ *
+ *
+ * @param source underlying stream from which is read.
+ * @param trailerLength the length of the trailer which is hold back (must be >= 0).
+ * @throws IOException initializing the trailer buffer failed.
+ */
+ public TrailerInputStream(final InputStream source, final int trailerLength)
+ throws IOException {
+ if (trailerLength < 0) {
+ throw new IllegalArgumentException("Trailer length must be >= 0: " + trailerLength);
+ }
+ this.source = source;
+ this.trailer = trailerLength == 0 ? IOUtils.EMPTY_BYTE_ARRAY : new byte[trailerLength];
+ IOUtils.readFully(this.source, this.trailer);
+ }
+
+ @Override
+ public int read() throws IOException {
+ // Does exactly on source read call.
+ // Copies this.trailer.length bytes if source is not EOF.
+ final int newByte = this.source.read();
+ if (newByte == IOUtils.EOF || this.trailer.length == 0) {
+ return newByte;
+ }
+ final int ret = this.trailer[0];
+ System.arraycopy(this.trailer, 1, this.trailer, 0, this.trailer.length - 1);
+ this.trailer[this.trailer.length - 1] = (byte) newByte;
+ return ret;
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ // Does at most 2 IOUtils.read calls to source.
+ // Copies at most 2 * this.trailer.length bytes.
+ // All other bytes are directly written to the target buffer.
+ if (off < 0 || len < 0 || b.length - off < len) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (len == 0) {
+ return 0;
+ }
+ final int readIntoBuffer;
+ int read;
+ // fist step: move trailer + read data
+ // overview - b: [---------], t: [1234] --> b: [1234abcde], t: [fghi]
+ if (len <= this.trailer.length) {
+ // 1 IOUtils.read calls to source, copies this.trailer.length bytes
+ // trailer can fill b, so only read into trailer needed
+ // b: [----], trailer: [123456789] --> b: [1234], trailer: [----56789]
+ System.arraycopy(this.trailer, 0, b, off, len);
+ readIntoBuffer = len;
+ // b: [1234], trailer: [----56789] --> b: [1234], trailer: [56789----]
+ System.arraycopy(this.trailer, len, this.trailer, 0, this.trailer.length - len);
+ // b: [1234], trailer: [56789----] --> b: [1234], trailer: [56789abcd]
+ read = IOUtils.read(this.source, this.trailer, this.trailer.length - len, len);
+ } else {
+ // 1 or 2 IOUtils.read calls to source, copies this.trailer.length bytes
+ // trailer smaller than b, so need to read into b and trailer
+ // b: [---------], t: [1234] --> b: [1234-----], t: [----]
+ System.arraycopy(this.trailer, 0, b, off, this.trailer.length);
+ // b: [1234-----], t: [----] --> b: [1234abcde], t: [----]
+ read = IOUtils.read(
+ this.source, b, off + this.trailer.length, len - this.trailer.length);
+ readIntoBuffer = this.trailer.length + read;
+ // b: [1234abcde], t: [----] --> b: [1234abcde], t: [fghi]
+ if (read == len - this.trailer.length) { // don't try reading data when stream source EOF
+ read += IOUtils.read(this.source, this.trailer);
+ }
+ }
+ // if less data than requested has been read, the trailer buffer is not full
+ // --> need to fill the trailer with the last bytes from b
+ // (only possible if we reached EOF)
+ // second step: ensure that trailer is completely filled with data
+ // overview - b: [abcdefghi], t: [jk--] --> b: [abcdefg--], t: [hijk]
+ final int underflow = Math.min(len - read, this.trailer.length);
+ if (underflow > 0) {
+ // at most this.trailer.length are copied to fill the trailer buffer
+ if (underflow < this.trailer.length) {
+ // trailer not completely empty, so move data to the end
+ // b: [abcdefghi], t: [jk--] --> b: [abcdefghi], t: [--jk]
+ System.arraycopy(
+ this.trailer, 0, this.trailer, underflow, this.trailer.length - underflow);
+ }
+ // fill trailer with last bytes from b
+ // b: [abcdefghi], t: [--jk] --> b: [abcdefg--], t: [hijk]
+ System.arraycopy(b, off + readIntoBuffer - underflow, this.trailer, 0, underflow);
+ }
+ // IOUtils.read reads as many bytes as possible, so reading 0 bytes means EOF.
+ // Then, we have to mark this.
+ return read == 0 && len != 0 ? IOUtils.EOF : read;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return this.source.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ this.source.close();
+ } finally {
+ super.close();
+ }
+ }
+
+ public int getTrailerLength() {
+ return this.trailer.length;
+ }
+
+ public byte[] copyTrailer() {
+ return this.trailer.clone();
+ }
+
+ public void copyTrailer(final byte[] target, final int off, final int len) {
+ System.arraycopy(this.trailer, 0, target, off, Math.min(len, this.trailer.length));
+ }
+
+ public void copyTrailer(final byte[] target) {
+ this.copyTrailer(target, 0, target.length);
+ }
+
+ public void copyTrailer(final OutputStream target) throws IOException {
+ target.write(this.trailer);
+ }
+}
diff --git a/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java b/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
new file mode 100644
index 00000000000..bd252a34b4b
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.function.IOConsumer;
+import org.apache.commons.io.output.WriterOutputStream;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class TrailerInputStreamTest {
+
+ private static class ChunkInputStream extends InputStream {
+
+ private final Iterator chunks;
+
+ public ChunkInputStream(final Iterator chunks) {
+ this.chunks = chunks;
+ }
+
+ @Override
+ public int read() throws IOException {
+ final byte[] buffer = new byte[1];
+ final int read = this.read(buffer);
+ if (read == IOUtils.EOF) {
+ return IOUtils.EOF;
+ }
+ return buffer[0];
+ }
+
+ @Override
+ public int read(final byte[] b, final int off, final int len) {
+ final byte[] chunk;
+ try {
+ chunk = this.chunks.next();
+ } catch (
+ @SuppressWarnings("unused")
+ final NoSuchElementException unused) {
+ return IOUtils.EOF;
+ }
+ Assertions.assertNotEquals(0, chunk.length);
+ Assertions.assertTrue(chunk.length <= len);
+ if (this.chunks.hasNext()) {
+ Assertions.assertEquals(chunk.length, len);
+ }
+ final int read = Math.min(chunk.length, len);
+ System.arraycopy(chunk, 0, b, off, read);
+ return read;
+ }
+
+ @Override
+ public void close() {
+ Assertions.assertFalse(this.chunks.hasNext());
+ }
+ }
+
+ public static Stream createTestStringChunkStream(
+ final int trailerLength,
+ final int chunkLength,
+ final int chunks,
+ final int lastChunkReduction) {
+ final List cs = new ArrayList<>();
+ char c = 'a';
+ if (trailerLength > 0) {
+ cs.add(StringUtils.repeat(c++, trailerLength));
+ }
+ for (int i = 0; i < chunks; i++) {
+ int cl = chunkLength;
+ if (i == chunkLength - 1) {
+ cl -= lastChunkReduction;
+ }
+ if (cl <= trailerLength || trailerLength == 0) {
+ cs.add(StringUtils.repeat(c++, cl));
+ } else {
+ cs.add(StringUtils.repeat(c++, cl - trailerLength));
+ cs.add(StringUtils.repeat(c++, trailerLength));
+ }
+ Assertions.assertTrue(c <= 'z');
+ }
+ return cs.stream();
+ }
+
+ public static Stream createTestBytesChunkStream(
+ final int trailerLength,
+ final int chunkLength,
+ final int chunks,
+ final int lastChunkReduction) {
+ return TrailerInputStreamTest.createTestStringChunkStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction)
+ .map(s -> s.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public static InputStream createTestInputStream(
+ final int trailerLength,
+ final int chunkLength,
+ final int chunks,
+ final int lastChunkReduction) {
+ return new ChunkInputStream(
+ TrailerInputStreamTest.createTestBytesChunkStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction)
+ .iterator());
+ }
+
+ public static String utf8String(
+ final IOConsumer super OutputStream> consumer) throws IOException {
+ try (StringWriter sw = new StringWriter();
+ WriterOutputStream wos = WriterOutputStream.builder().setCharset(StandardCharsets.UTF_8).setWriter(sw).get()) {
+ consumer.accept(wos);
+ wos.flush();
+ sw.flush();
+ return sw.toString();
+ }
+ }
+
+ public static void assertDataTrailer(
+ final int trailerLength,
+ final int chunkLength,
+ final int chunks,
+ final int lastChunkReduction,
+ final ByteArrayOutputStream os,
+ final TrailerInputStream tis)
+ throws IOException {
+ final String d =
+ TrailerInputStreamTest.createTestStringChunkStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction)
+ .collect(Collectors.joining());
+ final String data = d.substring(0, d.length() - trailerLength);
+ final String trailer = d.substring(d.length() - trailerLength);
+ os.flush();
+ Assertions.assertAll(
+ () -> Assertions.assertEquals(d, data + trailer, "Generation of expectation"),
+ () -> Assertions.assertEquals(trailerLength, trailer.length(), "Trailer length"),
+ () -> Assertions.assertEquals(data, utf8String(os::writeTo), "Data content"),
+ () -> Assertions.assertEquals(
+ trailer, utf8String(tis::copyTrailer), "Trailer content"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 5, 7, 10})
+ public void testReadBytewise(final int trailerLength) throws IOException {
+ final int chunkLength = 1;
+ final int chunks = 5;
+ final int lastChunkReduction = 0;
+ try (InputStream is =
+ TrailerInputStreamTest.createTestInputStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction);
+ TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
+ ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ Assertions.assertEquals(
+ StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ int read;
+ while ((read = tis.read()) != IOUtils.EOF) {
+ os.write(read);
+ }
+ assertDataTrailer(trailerLength, chunkLength, chunks, lastChunkReduction, os, tis);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 5, 7, 10})
+ public void testReadWholeBlocks(final int trailerLength) throws IOException {
+ final int chunkLength = 7;
+ final int chunks = 5;
+ final int lastChunkReduction = 0;
+ try (InputStream is =
+ TrailerInputStreamTest.createTestInputStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction);
+ TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
+ ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ Assertions.assertEquals(
+ StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ final byte[] buffer = new byte[chunkLength];
+ int read;
+ while ((read = tis.read(buffer)) != IOUtils.EOF) {
+ os.write(buffer, 0, read);
+ }
+ assertDataTrailer(trailerLength, chunkLength, chunks, lastChunkReduction, os, tis);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 5, 7, 10})
+ public void testReadLastBlockAlmostFull(final int trailerLength) throws IOException {
+ final int chunkLength = 7;
+ final int chunks = 5;
+ final int lastChunkReduction = 1;
+ try (InputStream is =
+ TrailerInputStreamTest.createTestInputStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction);
+ TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
+ ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ Assertions.assertEquals(
+ StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ final byte[] buffer = new byte[chunkLength + 3 * chunks];
+ int offset = chunks;
+ while (true) {
+ Arrays.fill(buffer, (byte) '?');
+ final int read = tis.read(buffer, offset, chunkLength);
+ if (read == IOUtils.EOF) {
+ break;
+ }
+ os.write(buffer, offset, read);
+ offset++;
+ }
+ assertDataTrailer(trailerLength, chunkLength, chunks, lastChunkReduction, os, tis);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 5, 7, 10})
+ public void testReadLastBlockAlmostEmpty(final int trailerLength) throws IOException {
+ final int chunkLength = 7;
+ final int chunks = 5;
+ final int lastChunkReduction = chunkLength - 1;
+ try (InputStream is =
+ TrailerInputStreamTest.createTestInputStream(
+ trailerLength, chunkLength, chunks, lastChunkReduction);
+ TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
+ ByteArrayOutputStream os = new ByteArrayOutputStream()) {
+ Assertions.assertEquals(
+ StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ final byte[] buffer = new byte[chunkLength + 3 * chunks];
+ int offset = chunks;
+ while (true) {
+ Arrays.fill(buffer, (byte) '?');
+ final int read = tis.read(buffer, offset, chunkLength);
+ if (read == IOUtils.EOF) {
+ break;
+ }
+ os.write(buffer, offset, read);
+ offset++;
+ }
+ assertDataTrailer(trailerLength, chunkLength, chunks, lastChunkReduction, os, tis);
+ }
+ }
+}
From 59d74a39707a96097ae1c850f9d2f5fc55131164 Mon Sep 17 00:00:00 2001
From: Andreas Loth
Date: Mon, 23 Oct 2023 10:04:13 +0200
Subject: [PATCH 2/4] [IO-427] Reduce TrailerInputStream#copyTrailer to only
one method
---
.../commons/io/input/TrailerInputStream.java | 12 --------
.../io/input/TrailerInputStreamTest.java | 30 +++++++------------
2 files changed, 11 insertions(+), 31 deletions(-)
diff --git a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
index 4c86cb5903d..ca826d7531b 100644
--- a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
@@ -15,7 +15,6 @@
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
/**
@@ -166,15 +165,4 @@ public byte[] copyTrailer() {
return this.trailer.clone();
}
- public void copyTrailer(final byte[] target, final int off, final int len) {
- System.arraycopy(this.trailer, 0, target, off, Math.min(len, this.trailer.length));
- }
-
- public void copyTrailer(final byte[] target) {
- this.copyTrailer(target, 0, target.length);
- }
-
- public void copyTrailer(final OutputStream target) throws IOException {
- target.write(this.trailer);
- }
}
diff --git a/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java b/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
index bd252a34b4b..60a8819d9ad 100644
--- a/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/TrailerInputStreamTest.java
@@ -16,8 +16,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
@@ -28,8 +26,6 @@
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.function.IOConsumer;
-import org.apache.commons.io.output.WriterOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
@@ -128,15 +124,11 @@ public static InputStream createTestInputStream(
.iterator());
}
- public static String utf8String(
- final IOConsumer super OutputStream> consumer) throws IOException {
- try (StringWriter sw = new StringWriter();
- WriterOutputStream wos = WriterOutputStream.builder().setCharset(StandardCharsets.UTF_8).setWriter(sw).get()) {
- consumer.accept(wos);
- wos.flush();
- sw.flush();
- return sw.toString();
- }
+ public static String trailerUtf8String(
+ final TrailerInputStream tis) {
+ final byte[] trailer = tis.copyTrailer();
+ Assertions.assertEquals(trailer.length, tis.getTrailerLength());
+ return new String(trailer, 0, trailer.length, StandardCharsets.UTF_8);
}
public static void assertDataTrailer(
@@ -157,9 +149,9 @@ public static void assertDataTrailer(
Assertions.assertAll(
() -> Assertions.assertEquals(d, data + trailer, "Generation of expectation"),
() -> Assertions.assertEquals(trailerLength, trailer.length(), "Trailer length"),
- () -> Assertions.assertEquals(data, utf8String(os::writeTo), "Data content"),
+ () -> Assertions.assertEquals(data, os.toString(StandardCharsets.UTF_8.name()), "Data content"),
() -> Assertions.assertEquals(
- trailer, utf8String(tis::copyTrailer), "Trailer content"));
+ trailer, trailerUtf8String(tis), "Trailer content"));
}
@ParameterizedTest
@@ -174,7 +166,7 @@ public void testReadBytewise(final int trailerLength) throws IOException {
TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Assertions.assertEquals(
- StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ StringUtils.repeat('a', trailerLength), trailerUtf8String(tis));
int read;
while ((read = tis.read()) != IOUtils.EOF) {
os.write(read);
@@ -195,7 +187,7 @@ public void testReadWholeBlocks(final int trailerLength) throws IOException {
TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Assertions.assertEquals(
- StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ StringUtils.repeat('a', trailerLength), trailerUtf8String(tis));
final byte[] buffer = new byte[chunkLength];
int read;
while ((read = tis.read(buffer)) != IOUtils.EOF) {
@@ -217,7 +209,7 @@ public void testReadLastBlockAlmostFull(final int trailerLength) throws IOExcept
TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Assertions.assertEquals(
- StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ StringUtils.repeat('a', trailerLength), trailerUtf8String(tis));
final byte[] buffer = new byte[chunkLength + 3 * chunks];
int offset = chunks;
while (true) {
@@ -245,7 +237,7 @@ public void testReadLastBlockAlmostEmpty(final int trailerLength) throws IOExcep
TrailerInputStream tis = new TrailerInputStream(is, trailerLength);
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Assertions.assertEquals(
- StringUtils.repeat('a', trailerLength), utf8String(tis::copyTrailer));
+ StringUtils.repeat('a', trailerLength), trailerUtf8String(tis));
final byte[] buffer = new byte[chunkLength + 3 * chunks];
int offset = chunks;
while (true) {
From edc394eccd2ca07b7cf16ca7cc445295f669cf7f Mon Sep 17 00:00:00 2001
From: Andreas Loth
Date: Mon, 23 Oct 2023 11:26:44 +0200
Subject: [PATCH 3/4] [IO-427] Mention and explain that TrailerInputStream
lacks support of mark/reset
---
.../commons/io/input/TrailerInputStream.java | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
index ca826d7531b..d65c5d1678d 100644
--- a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
@@ -36,9 +36,24 @@
* streaming the payload the trailer finally contains the expected hash (this example needs
* extra caution to revert actions when the final checksum match fails).
*
+ *
+ *
+ * No mark/reset support.
+ *
+ *
+ *
+ * Not thread-safe. If accessed by multiple threads concurrently, external synchronization is
+ * necessary.
+ *
*/
public final class TrailerInputStream extends InputStream {
+ // The current implementation is incompatible with mark/reset as it doesn't track which bytes are
+ // already read and which ones are new. This tracking would be necessary to not overwrite the
+ // trailer with earlier bytes in the source stream. Remember that the trailer is not meant to
+ // contain the last read bytes but the last bytes in the stream (which differs when using reset
+ // to jump to an earlier position of the source stream).
+
private final InputStream source;
/**
* Invariant: After every method call which exited without exception, the trailer has to be
From 7c5cbb39015b8fc6d9feb66223a4c5e175c519ca Mon Sep 17 00:00:00 2001
From: Andreas Loth
Date: Mon, 23 Oct 2023 12:26:39 +0200
Subject: [PATCH 4/4] [IO-427] Explain super class choice for
TrailerInputStream
---
.../apache/commons/io/input/TrailerInputStream.java | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
index d65c5d1678d..dd7d36c83ea 100644
--- a/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/TrailerInputStream.java
@@ -48,6 +48,16 @@
*/
public final class TrailerInputStream extends InputStream {
+ // Extending FilterInputStream or ProxyInputStream would save overriding
+ // * close, and
+ // * available
+ // but would require to override
+ // * mark,
+ // * reset, and
+ // * markSupported.
+ // So, there is no benefit in extending FilterInputStream or ProxyInputStream over InputStream
+ // as mark/reset is not supported by this implementation.
+
// The current implementation is incompatible with mark/reset as it doesn't track which bytes are
// already read and which ones are new. This tracking would be necessary to not overwrite the
// trailer with earlier bytes in the source stream. Remember that the trailer is not meant to