/* Copyright 2014 Duncan Jones * * 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.cryptonode.jncryptor; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; /** * A wrapper for an input stream that contains trailer data. */ class TrailerInputStream extends InputStream { /** * Byte value indicating the end of stream. */ private static final int EOF_VALUE = -1; private final int trailerSize; private final InputStream in; private byte[] trailerBuffer; /** * Creates a {@code TrailerInputStream} that wraps another stream. * * @param in * the stream to read from * @param trailerSize * the byte length of the trailer */ public TrailerInputStream(InputStream in, int trailerSize) { Validate.notNull(in, "InputStream cannot be null."); Validate.isTrue(trailerSize > -1, "Trailer size cannot be negative."); this.in = in; this.trailerSize = trailerSize; } /** * Populates the trailer buffer with data from the stream. * * @throws EOFException * if the stream finishes before the trailer is read * @throws IOException * if the underlying stream throws an exception */ private void fillTrailerBuffer() throws IOException { trailerBuffer = new byte[trailerSize]; if (trailerSize == 0) { return; } int bytesRead = StreamUtils.readAllBytes(in, trailerBuffer); if (bytesRead != trailerBuffer.length) { throw new EOFException(String.format( "Trailer size was %d bytes but stream only contained %d bytes.", trailerSize, bytesRead)); } } /** * Reads the next byte from the underlying {@code InputStream}. This method * returns {@code -1} when the last byte before the trailer has been read. * * @return the next non-trailer byte from the underlying stream, or {@code -1} * * @throws EOFException * if the stream contains less data than the size of the trailer * @throws IOException * if the underlying stream throws an exception */ @Override public int read() throws IOException { if (trailerBuffer == null) { fillTrailerBuffer(); } int nextByte = in.read(); if (nextByte == EOF_VALUE) { return nextByte; } if (trailerBuffer.length == 0) { return nextByte; } int result = trailerBuffer[0] & 0xFF; // must be positive System.arraycopy(trailerBuffer, 1, trailerBuffer, 0, trailerBuffer.length - 1); trailerBuffer[trailerBuffer.length - 1] = (byte) nextByte; return result; } /** * Has the same affect as {@code read(b, 0, b.length)}. */ @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } /** * Reads up to {@code len} non-trailer bytes from the underlying stream into * the array {@code b}, beginning at offset {@code off}. * * @param b * the buffer into which the data is read. * @param off * the start offset in array {@code b} at which the data is written. * @param len * the maximum number of bytes to read. * @return the total number of bytes read into the buffer, or {@code -1} if * there is no more data because the end of the stream has been * reached. * * @throws IOException * If the first byte cannot be read for any reason other than end of * file, or if the input stream has been closed, or if there are * insufficient bytes in the underlying stream to read the trailer, * or if some other I/O error occurs. */ @Override public int read(byte[] b, int off, int len) throws IOException { // Sanity checks taken from InputStream if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } if (trailerBuffer == null) { fillTrailerBuffer(); } byte[] inputBuffer = new byte[len]; int numBytesRead = in.read(inputBuffer); if (numBytesRead == EOF_VALUE) { return numBytesRead; } if (trailerSize == 0) { System.arraycopy(inputBuffer, 0, b, off, numBytesRead); return numBytesRead; } if (numBytesRead <= trailerSize) { // Need some of the trailer System.arraycopy(trailerBuffer, 0, b, off, numBytesRead); // Now need to shift rear of trailer to front System.arraycopy(trailerBuffer, numBytesRead, trailerBuffer, 0, trailerSize - numBytesRead); // Now need to fill rear of trailer System.arraycopy(inputBuffer, 0, trailerBuffer, trailerSize - numBytesRead, numBytesRead); } else { // Need all the trailer System.arraycopy(trailerBuffer, 0, b, off, trailerSize); off += trailerSize; // Need the remaining data, except enough to fill the buffer System.arraycopy(inputBuffer, 0, b, off, numBytesRead - trailerSize); // Finally, fill the buffer from the remaining input System.arraycopy(inputBuffer, numBytesRead - trailerSize, trailerBuffer, 0, trailerSize); } return numBytesRead; } @Override public int available() throws IOException { if (trailerBuffer == null) { return Math.max(0, in.available() - trailerSize); } return in.available(); } @Override public void close() throws IOException { in.close(); } /** * The {@code mark} and {@code reset} methods are not supported in this * stream. * * @return {@code false} */ @Override public boolean markSupported() { return false; } /** * Returns the trailer data. The result of this method will be inaccurate * unless the entire underlying stream has been read. * * @return the trailer data, or {@code null} if no bytes have been read using * the {@code read} methods */ public byte[] getTrailer() { return trailerBuffer.clone(); } }