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 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 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