// Copyright 2006 Google Inc. // // 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 com.google.enterprise.connector.util; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * A {@code FilterInputStream} that Base64 encodes data read from an * input stream. * * @since 2.8 */ public class Base64FilterInputStream extends FilterInputStream { // NOTE: Since output line position is not maintained across calls // to read(...), the actual length of output lines might exceed this. // For our purposes, strict adherence to RFC 2045 is not necessary. // In practice, however, our sole use of read(byte[], int, int) with // large buffer sizes will produce consistent results. We produce 76 // character lines for the benefit of third-party decoders that might // be used when debugging. static final int BASE64_LINE_LENGTH = 76; private boolean breakLines = false; /** * Given some {@code InputStream}, create an {@code InputStream} that Base64 * encodes the input stream. No line breaks are included in the output. * * @param in an {@code InputStream} providing source data for encoding */ public Base64FilterInputStream(InputStream in) { this (in, false); } /** * Given some {@code InputStream}, create an {@code InputStream} that Base64 * encodes the input stream. * * @param in an InputStream providing source data for encoding * @param breakLines if true, add line breaks in the output */ public Base64FilterInputStream(InputStream in, boolean breakLines) { super(in); this.breakLines = breakLines; } /* This is used when reading small amounts of data. */ private byte[] inputBuffer = new byte[3]; private byte[] encodedBuffer = new byte[4]; private int encodedBufPos = 4; // Position of next byte to read. /** * Reads the next byte of data from the input stream and returns it. * Returns (int) -1 if at EOF. */ @Override public int read() throws IOException { if (encodedBufPos >= 4) { int retVal = fillbuff(inputBuffer, 0, 3); if (-1 == retVal) { return -1; } else { Base64.encode3to4(inputBuffer, 0, retVal, encodedBuffer, 0, Base64.ALPHABET); encodedBufPos = 0; } } return encodedBuffer[encodedBufPos++]; } /** * Reads up to {@code len} bytes of data from the input stream into an array * of bytes. Returns the actual number of bytes written to the array. */ @Override public int read(byte b[], int off, int len) throws IOException { // If there is some leftover morsel of encoded data, return that. if (len > 3 && encodedBufPos < 4) { len = 4 - encodedBufPos; } // Special case reads of less than one encoded quadbyte. int bytesWritten = 0; if (len < 4) { for (; bytesWritten < len; bytesWritten++) { int aByte = read(); if (aByte == -1) { return (bytesWritten > 0) ? bytesWritten : -1; } else { b[off + bytesWritten] = (byte)aByte; } } return bytesWritten; } // Determine the number of threebyte datum we need to read to // fill the destination buffer with quadbyte encoded data. // If we are breaking lines, try to constrain the read size // so that it generates whole lines. int readLen; int lineLen; if (breakLines && len > BASE64_LINE_LENGTH) { readLen = (((len / (BASE64_LINE_LENGTH+1)) * BASE64_LINE_LENGTH) / 4) * 3; lineLen = BASE64_LINE_LENGTH; } else { readLen = (len / 4) * 3; lineLen = Integer.MAX_VALUE; } // Read the input data into the tail end of the target buffer. int readBytes = fillbuff(b, off + (len - readLen), readLen); if (readBytes == -1) { return -1; } // Convert the buffer in-place. bytesWritten = Base64.encode(b, off + (len - readLen), readBytes, b, off, Base64.ALPHABET, lineLen); return bytesWritten; } /** * Try to fill up the buffer with data read from the input stream. * This is tolerant of short reads - returning less than the requested * amount of data, even if there is more available. * * @param b buffer to fill * @param off offset into b to start filling * @param len number of bytes to read * @return number of bytes written to buffer b, or -1 if at EOF */ private int fillbuff(byte b[], int off, int len) throws IOException { int bytesRead = 0; while (bytesRead < len) { int val = in.read(b, off + bytesRead, len - bytesRead); if (val == -1) { return (bytesRead > 0) ? bytesRead : -1; } bytesRead += val; } return bytesRead; } /** * This implementation does not support {@code mark()} or {@code reset()}. * * @return {@code false} */ @Override public boolean markSupported() { return false; } /** * Return the number of bytes available to read. */ @Override public int available() throws IOException { int available = ((in.available() + 2) / 3) * 4; if (encodedBufPos < 4) { available += (4 - encodedBufPos); } if (breakLines) { available += available/BASE64_LINE_LENGTH; } return available; } /** * Skip over bytes in the input stream. * * @param n number of bytes to skip * @return number of bytes skipped */ @Override public long skip(long n) throws IOException { long skipped = 0; if (breakLines) { n -= n/BASE64_LINE_LENGTH; } // Skip over encoded morsel. while (encodedBufPos < 4 && n > 0) { encodedBufPos++; skipped++; n--; } // Skip over enough threebytes to cover the resulting encoded quadbytes. if (n > 0) { skipped += in.skip((n / 4) * 3); } return skipped; } }