Skip to content

Commit 46892fc

Browse files
authored
Add ChecksumInputStream and test (#548)
Should replace the Apache Commons Compress version
1 parent c54c044 commit 46892fc

2 files changed

Lines changed: 405 additions & 0 deletions

File tree

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.io.input;
18+
19+
import static org.apache.commons.io.IOUtils.EOF;
20+
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.util.zip.CheckedInputStream;
24+
import java.util.zip.Checksum;
25+
26+
import org.apache.commons.io.build.AbstractStreamBuilder;
27+
28+
/**
29+
* Automatically verifies a {@link Checksum} value once the stream is exhausted or the count threshold is reached.
30+
* <p>
31+
* If the {@link Checksum} does not meet the expected value when exhausted, then the input stream throws an
32+
* {@link IOException}.
33+
* </p>
34+
* <p>
35+
* If you do not need the verification or threshold feature, then use a plain {@link CheckedInputStream}.
36+
* </p>
37+
*
38+
* @since 2.16.0
39+
*/
40+
public final class ChecksumInputStream extends CountingInputStream {
41+
42+
// @formatter:off
43+
/**
44+
* Builds a new {@link ChecksumInputStream} instance.
45+
* <p>
46+
* There is no default {@link Checksum}; you MUST provide one.
47+
* </p>
48+
* <h2>Using NIO</h2>
49+
*
50+
* <pre>{@code
51+
* ChecksumInputStream s = ChecksumInputStream.builder()
52+
* .setPath(Paths.get("MyFile.xml"))
53+
* .setChecksum(new CRC32())
54+
* .setExpectedChecksumValue(12345)
55+
* .get();
56+
* }</pre>
57+
*
58+
* <h2>Using IO</h2>
59+
*
60+
* <pre>{@code
61+
* ChecksumInputStream s = ChecksumInputStream.builder()
62+
* .setFile(new File("MyFile.xml"))
63+
* .setChecksum(new CRC32())
64+
* .setExpectedChecksumValue(12345)
65+
* .get();
66+
* }</pre>
67+
*
68+
* <h2>Validating only part of an InputStream</h2>
69+
* <p>
70+
* The following validates the first 100 bytes of the given input.
71+
* </p>
72+
* <pre>{@code
73+
* ChecksumInputStream s = ChecksumInputStream.builder()
74+
* .setPath(Paths.get("MyFile.xml"))
75+
* .setChecksum(new CRC32())
76+
* .setExpectedChecksumValue(12345)
77+
* .setCountThreshold(100)
78+
* .get();
79+
* }</pre>
80+
* <p>
81+
* To validate input <em>after</em> the beginning of a stream, build an instance with an InputStream starting where you want to validate.
82+
* </p>
83+
* <pre>{@code
84+
* InputStream inputStream = ...;
85+
* inputStream.read(...);
86+
* inputStream.skip(...);
87+
* ChecksumInputStream s = ChecksumInputStream.builder()
88+
* .setInputStream(inputStream)
89+
* .setChecksum(new CRC32())
90+
* .setExpectedChecksumValue(12345)
91+
* .setCountThreshold(100)
92+
* .get();
93+
* }</pre>
94+
*/
95+
// @formatter:on
96+
public static class Builder extends AbstractStreamBuilder<ChecksumInputStream, Builder> {
97+
98+
/**
99+
* There is no default checksum, you MUST provide one. This avoids any issue with a default {@link Checksum}
100+
* being proven deficient or insecure in the future.
101+
*/
102+
private Checksum checksum;
103+
104+
/**
105+
* The count threshold to limit how much input is consumed to update the {@link Checksum} before the input
106+
* stream validates its value.
107+
* <p>
108+
* By default, all input updates the {@link Checksum}.
109+
* </p>
110+
*/
111+
private long countThreshold = -1;
112+
113+
/**
114+
* The expected {@link Checksum} value once the stream is exhausted or the count threshold is reached.
115+
*/
116+
private long expectedChecksumValue;
117+
118+
/**
119+
* Constructs a new instance.
120+
* <p>
121+
* This builder requires an input convertible by {@link #getInputStream()}.
122+
* </p>
123+
* <p>
124+
* You must provide an origin that can be converted to an InputStream by this builder, otherwise, this call will
125+
* throw an {@link UnsupportedOperationException}.
126+
* </p>
127+
*
128+
* @return a new instance.
129+
* @throws UnsupportedOperationException if the origin cannot provide an InputStream.
130+
* @see #getInputStream()
131+
*/
132+
@SuppressWarnings("resource")
133+
@Override
134+
public ChecksumInputStream get() throws IOException {
135+
return new ChecksumInputStream(getInputStream(), checksum, expectedChecksumValue, countThreshold);
136+
}
137+
138+
/**
139+
* Sets the Checksum.
140+
*
141+
* @param checksum the Checksum.
142+
* @return this.
143+
*/
144+
public Builder setChecksum(final Checksum checksum) {
145+
this.checksum = checksum;
146+
return this;
147+
}
148+
149+
/**
150+
* Sets the count threshold to limit how much input is consumed to update the {@link Checksum} before the input
151+
* stream validates its value.
152+
* <p>
153+
* By default, all input updates the {@link Checksum}.
154+
* </p>
155+
*
156+
* @param countThreshold the count threshold. A negative number means the threshold is unbound.
157+
* @return this.
158+
*/
159+
public Builder setCountThreshold(final long countThreshold) {
160+
this.countThreshold = countThreshold;
161+
return this;
162+
}
163+
164+
/**
165+
* The expected {@link Checksum} value once the stream is exhausted or the count threshold is reached.
166+
*
167+
* @param expectedChecksumValue The expected Checksum value.
168+
* @return this.
169+
*/
170+
public Builder setExpectedChecksumValue(final long expectedChecksumValue) {
171+
this.expectedChecksumValue = expectedChecksumValue;
172+
return this;
173+
}
174+
175+
}
176+
177+
/**
178+
* Constructs a new {@link Builder}.
179+
*
180+
* @return a new {@link Builder}.
181+
*/
182+
public static Builder builder() {
183+
return new Builder();
184+
}
185+
186+
/** The expected checksum. */
187+
private final long expectedChecksumValue;
188+
189+
/**
190+
* The count threshold to limit how much input is consumed to update the {@link Checksum} before the input stream
191+
* validates its value.
192+
* <p>
193+
* By default, all input updates the {@link Checksum}.
194+
* </p>
195+
*/
196+
private final long countThreshold;
197+
198+
/**
199+
* Constructs a new instance.
200+
*
201+
* @param in the stream to wrap.
202+
* @param checksum a Checksum implementation.
203+
* @param expectedChecksumValue the expected checksum.
204+
* @param countThreshold the count threshold to limit how much input is consumed, a negative number means the
205+
* threshold is unbound.
206+
*/
207+
private ChecksumInputStream(final InputStream in, final Checksum checksum, final long expectedChecksumValue,
208+
final long countThreshold) {
209+
super(new CheckedInputStream(in, checksum));
210+
this.countThreshold = countThreshold;
211+
this.expectedChecksumValue = expectedChecksumValue;
212+
}
213+
214+
@Override
215+
protected synchronized void afterRead(final int n) throws IOException {
216+
super.afterRead(n);
217+
if ((countThreshold > 0 && getByteCount() >= countThreshold || n == EOF)
218+
&& expectedChecksumValue != getChecksum().getValue()) {
219+
// Validate when past the threshold or at EOF
220+
throw new IOException("Checksum verification failed.");
221+
}
222+
}
223+
224+
/**
225+
* Gets the current checksum value.
226+
*
227+
* @return the current checksum value.
228+
*/
229+
private Checksum getChecksum() {
230+
return ((CheckedInputStream) in).getChecksum();
231+
}
232+
233+
/**
234+
* Gets the byte count remaining to read.
235+
*
236+
* @return bytes remaining to read, a negative number means the threshold is unbound.
237+
*/
238+
public long getRemaining() {
239+
return countThreshold - getByteCount();
240+
}
241+
242+
}

0 commit comments

Comments
 (0)