blob: f1a25517fdc5cdf33082f7de0da8fd51a55f1510 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 static org.apache.commons.io.IOUtils.EOF;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;
import org.apache.commons.io.build.AbstractStreamBuilder;
/**
* Automatically verifies a {@link Checksum} value once the stream is exhausted or the count threshold is reached.
* <p>
* If the {@link Checksum} does not meet the expected value when exhausted, then the input stream throws an
* {@link IOException}.
* </p>
* <p>
* If you do not need the verification or threshold feature, then use a plain {@link CheckedInputStream}.
* </p>
*
* @since 2.16.0
*/
public final class ChecksumInputStream extends CountingInputStream {
// @formatter:off
/**
* Builds a new {@link ChecksumInputStream} instance.
* <p>
* There is no default {@link Checksum}; you MUST provide one.
* </p>
* <h2>Using NIO</h2>
*
* <pre>{@code
* ChecksumInputStream s = ChecksumInputStream.builder()
* .setPath(Paths.get("MyFile.xml"))
* .setChecksum(new CRC32())
* .setExpectedChecksumValue(12345)
* .get();
* }</pre>
*
* <h2>Using IO</h2>
*
* <pre>{@code
* ChecksumInputStream s = ChecksumInputStream.builder()
* .setFile(new File("MyFile.xml"))
* .setChecksum(new CRC32())
* .setExpectedChecksumValue(12345)
* .get();
* }</pre>
*
* <h2>Validating only part of an InputStream</h2>
* <p>
* The following validates the first 100 bytes of the given input.
* </p>
* <pre>{@code
* ChecksumInputStream s = ChecksumInputStream.builder()
* .setPath(Paths.get("MyFile.xml"))
* .setChecksum(new CRC32())
* .setExpectedChecksumValue(12345)
* .setCountThreshold(100)
* .get();
* }</pre>
* <p>
* To validate input <em>after</em> the beginning of a stream, build an instance with an InputStream starting where you want to validate.
* </p>
* <pre>{@code
* InputStream inputStream = ...;
* inputStream.read(...);
* inputStream.skip(...);
* ChecksumInputStream s = ChecksumInputStream.builder()
* .setInputStream(inputStream)
* .setChecksum(new CRC32())
* .setExpectedChecksumValue(12345)
* .setCountThreshold(100)
* .get();
* }</pre>
*/
// @formatter:on
public static class Builder extends AbstractStreamBuilder<ChecksumInputStream, Builder> {
/**
* There is no default checksum, you MUST provide one. This avoids any issue with a default {@link Checksum}
* being proven deficient or insecure in the future.
*/
private Checksum checksum;
/**
* The count threshold to limit how much input is consumed to update the {@link Checksum} before the input
* stream validates its value.
* <p>
* By default, all input updates the {@link Checksum}.
* </p>
*/
private long countThreshold = -1;
/**
* The expected {@link Checksum} value once the stream is exhausted or the count threshold is reached.
*/
private long expectedChecksumValue;
/**
* Constructs a new instance.
* <p>
* This builder requires an input convertible by {@link #getInputStream()}.
* </p>
* <p>
* You must provide an origin that can be converted to an InputStream by this builder, otherwise, this call will
* throw an {@link UnsupportedOperationException}.
* </p>
*
* @return a new instance.
* @throws UnsupportedOperationException if the origin cannot provide an InputStream.
* @see #getInputStream()
*/
@SuppressWarnings("resource")
@Override
public ChecksumInputStream get() throws IOException {
return new ChecksumInputStream(getInputStream(), checksum, expectedChecksumValue, countThreshold);
}
/**
* Sets the Checksum.
*
* @param checksum the Checksum.
* @return this.
*/
public Builder setChecksum(final Checksum checksum) {
this.checksum = checksum;
return this;
}
/**
* Sets the count threshold to limit how much input is consumed to update the {@link Checksum} before the input
* stream validates its value.
* <p>
* By default, all input updates the {@link Checksum}.
* </p>
*
* @param countThreshold the count threshold. A negative number means the threshold is unbound.
* @return this.
*/
public Builder setCountThreshold(final long countThreshold) {
this.countThreshold = countThreshold;
return this;
}
/**
* The expected {@link Checksum} value once the stream is exhausted or the count threshold is reached.
*
* @param expectedChecksumValue The expected Checksum value.
* @return this.
*/
public Builder setExpectedChecksumValue(final long expectedChecksumValue) {
this.expectedChecksumValue = expectedChecksumValue;
return this;
}
}
/**
* Constructs a new {@link Builder}.
*
* @return a new {@link Builder}.
*/
public static Builder builder() {
return new Builder();
}
/** The expected checksum. */
private final long expectedChecksumValue;
/**
* The count threshold to limit how much input is consumed to update the {@link Checksum} before the input stream
* validates its value.
* <p>
* By default, all input updates the {@link Checksum}.
* </p>
*/
private final long countThreshold;
/**
* Constructs a new instance.
*
* @param in the stream to wrap.
* @param checksum a Checksum implementation.
* @param expectedChecksumValue the expected checksum.
* @param countThreshold the count threshold to limit how much input is consumed, a negative number means the
* threshold is unbound.
*/
private ChecksumInputStream(final InputStream in, final Checksum checksum, final long expectedChecksumValue,
final long countThreshold) {
super(new CheckedInputStream(in, checksum));
this.countThreshold = countThreshold;
this.expectedChecksumValue = expectedChecksumValue;
}
@Override
protected synchronized void afterRead(final int n) throws IOException {
super.afterRead(n);
if ((countThreshold > 0 && getByteCount() >= countThreshold || n == EOF)
&& expectedChecksumValue != getChecksum().getValue()) {
// Validate when past the threshold or at EOF
throw new IOException("Checksum verification failed.");
}
}
/**
* Gets the current checksum value.
*
* @return the current checksum value.
*/
private Checksum getChecksum() {
return ((CheckedInputStream) in).getChecksum();
}
/**
* Gets the byte count remaining to read.
*
* @return bytes remaining to read, a negative number means the threshold is unbound.
*/
public long getRemaining() {
return countThreshold - getByteCount();
}
}