Skip to content

Commit 14639d2

Browse files
committed
[IO-427] Add TrailerInputStream
1 parent 92cf561 commit 14639d2

2 files changed

Lines changed: 443 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.apache.commons.io.input;
15+
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.io.OutputStream;
19+
import org.apache.commons.io.IOUtils;
20+
21+
/**
22+
* Reads the underlying input stream while holding back the trailer.
23+
*
24+
* <p>
25+
* "Normal" read calls read the underlying stream except the last few bytes (the trailer). The
26+
* trailer is updated with each read call. The trailer can be gotten by one of the copyTrailer
27+
* overloads.
28+
* </p>
29+
*
30+
* <p>
31+
* It is safe to fetch the trailer at any time but the trailer will change with each read call
32+
* until the underlying stream is EOF.
33+
* </p>
34+
*
35+
* <p>
36+
* Useful, e.g., for handling checksums: payload is followed by a fixed size hash, so while
37+
* streaming the payload the trailer finally contains the expected hash (this example needs
38+
* extra caution to revert actions when the final checksum match fails).
39+
* </p>
40+
*/
41+
public final class TrailerInputStream extends InputStream {
42+
43+
private final InputStream source;
44+
/**
45+
* Invariant: After every method call which exited without exception, the trailer has to be
46+
* completely filled.
47+
*/
48+
private final byte[] trailer;
49+
50+
/**
51+
* Constructs the TrailerInputStream and initializes the trailer buffer.
52+
*
53+
* <p>
54+
* Reads exactly {@code trailerLength} bytes from {@code source}.
55+
* </p>
56+
*
57+
* @param source underlying stream from which is read.
58+
* @param trailerLength the length of the trailer which is hold back (must be &gt;= 0).
59+
* @throws IOException initializing the trailer buffer failed.
60+
*/
61+
public TrailerInputStream(final InputStream source, final int trailerLength)
62+
throws IOException {
63+
if (trailerLength < 0) {
64+
throw new IllegalArgumentException("Trailer length must be >= 0: " + trailerLength);
65+
}
66+
this.source = source;
67+
this.trailer = trailerLength == 0 ? IOUtils.EMPTY_BYTE_ARRAY : new byte[trailerLength];
68+
IOUtils.readFully(this.source, this.trailer);
69+
}
70+
71+
@Override
72+
public int read() throws IOException {
73+
// Does exactly on source read call.
74+
// Copies this.trailer.length bytes if source is not EOF.
75+
final int newByte = this.source.read();
76+
if (newByte == IOUtils.EOF || this.trailer.length == 0) {
77+
return newByte;
78+
}
79+
final int ret = this.trailer[0];
80+
System.arraycopy(this.trailer, 1, this.trailer, 0, this.trailer.length - 1);
81+
this.trailer[this.trailer.length - 1] = (byte) newByte;
82+
return ret;
83+
}
84+
85+
@Override
86+
public int read(final byte[] b, final int off, final int len) throws IOException {
87+
// Does at most 2 IOUtils.read calls to source.
88+
// Copies at most 2 * this.trailer.length bytes.
89+
// All other bytes are directly written to the target buffer.
90+
if (off < 0 || len < 0 || b.length - off < len) {
91+
throw new IndexOutOfBoundsException();
92+
}
93+
if (len == 0) {
94+
return 0;
95+
}
96+
final int readIntoBuffer;
97+
int read;
98+
// fist step: move trailer + read data
99+
// overview - b: [---------], t: [1234] --> b: [1234abcde], t: [fghi]
100+
if (len <= this.trailer.length) {
101+
// 1 IOUtils.read calls to source, copies this.trailer.length bytes
102+
// trailer can fill b, so only read into trailer needed
103+
// b: [----], trailer: [123456789] --> b: [1234], trailer: [----56789]
104+
System.arraycopy(this.trailer, 0, b, off, len);
105+
readIntoBuffer = len;
106+
// b: [1234], trailer: [----56789] --> b: [1234], trailer: [56789----]
107+
System.arraycopy(this.trailer, len, this.trailer, 0, this.trailer.length - len);
108+
// b: [1234], trailer: [56789----] --> b: [1234], trailer: [56789abcd]
109+
read = IOUtils.read(this.source, this.trailer, this.trailer.length - len, len);
110+
} else {
111+
// 1 or 2 IOUtils.read calls to source, copies this.trailer.length bytes
112+
// trailer smaller than b, so need to read into b and trailer
113+
// b: [---------], t: [1234] --> b: [1234-----], t: [----]
114+
System.arraycopy(this.trailer, 0, b, off, this.trailer.length);
115+
// b: [1234-----], t: [----] --> b: [1234abcde], t: [----]
116+
read = IOUtils.read(
117+
this.source, b, off + this.trailer.length, len - this.trailer.length);
118+
readIntoBuffer = this.trailer.length + read;
119+
// b: [1234abcde], t: [----] --> b: [1234abcde], t: [fghi]
120+
if (read == len - this.trailer.length) { // don't try reading data when stream source EOF
121+
read += IOUtils.read(this.source, this.trailer);
122+
}
123+
}
124+
// if less data than requested has been read, the trailer buffer is not full
125+
// --> need to fill the trailer with the last bytes from b
126+
// (only possible if we reached EOF)
127+
// second step: ensure that trailer is completely filled with data
128+
// overview - b: [abcdefghi], t: [jk--] --> b: [abcdefg--], t: [hijk]
129+
final int underflow = Math.min(len - read, this.trailer.length);
130+
if (underflow > 0) {
131+
// at most this.trailer.length are copied to fill the trailer buffer
132+
if (underflow < this.trailer.length) {
133+
// trailer not completely empty, so move data to the end
134+
// b: [abcdefghi], t: [jk--] --> b: [abcdefghi], t: [--jk]
135+
System.arraycopy(
136+
this.trailer, 0, this.trailer, underflow, this.trailer.length - underflow);
137+
}
138+
// fill trailer with last bytes from b
139+
// b: [abcdefghi], t: [--jk] --> b: [abcdefg--], t: [hijk]
140+
System.arraycopy(b, off + readIntoBuffer - underflow, this.trailer, 0, underflow);
141+
}
142+
// IOUtils.read reads as many bytes as possible, so reading 0 bytes means EOF.
143+
// Then, we have to mark this.
144+
return read == 0 && len != 0 ? IOUtils.EOF : read;
145+
}
146+
147+
@Override
148+
public int available() throws IOException {
149+
return this.source.available();
150+
}
151+
152+
@Override
153+
public void close() throws IOException {
154+
try {
155+
this.source.close();
156+
} finally {
157+
super.close();
158+
}
159+
}
160+
161+
public int getTrailerLength() {
162+
return this.trailer.length;
163+
}
164+
165+
public byte[] copyTrailer() {
166+
return this.trailer.clone();
167+
}
168+
169+
public void copyTrailer(final byte[] target, final int off, final int len) {
170+
System.arraycopy(this.trailer, 0, target, off, Math.min(len, this.trailer.length));
171+
}
172+
173+
public void copyTrailer(final byte[] target) {
174+
this.copyTrailer(target, 0, target.length);
175+
}
176+
177+
public void copyTrailer(final OutputStream target) throws IOException {
178+
target.write(this.trailer);
179+
}
180+
}

0 commit comments

Comments
 (0)