/*
* Copyright 1999,2005 The Apache Software Foundation.
*
* 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.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.UndeclaredThrowableException;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/** Performs Base64 encoding and/or decoding. This is an on-the-fly decoder: Unlike,
* for example, the commons-codec classes, it doesn't depend on byte arrays. In
* other words, it has an extremely low memory profile. This is well suited even
* for very large byte streams.
*/
public class Base64 {
/** An exception of this type is thrown, if the decoded
* character stream contains invalid input.
*/
public static class DecodingException extends IOException {
private static final long serialVersionUID = 3257006574836135478L;
DecodingException(String pMessage) { super(pMessage); }
}
/** An exception of this type is thrown by the {@link SAXEncoder},
* if writing to the target handler causes a SAX exception.
* This class is required, because the {@link IOException}
* allows no cause until Java 1.3.
*/
public static class SAXIOException extends IOException {
private static final long serialVersionUID = 3258131345216451895L;
final SAXException saxException;
SAXIOException(SAXException e) {
super();
saxException = e;
}
/** Returns the encapsulated {@link SAXException}.
* @return An exception, which was thrown when invoking
* {@link ContentHandler#characters(char[], int, int)}.
*/
public SAXException getSAXException() { return saxException; }
}
/** Default line separator: \n
*/
public static final String LINE_SEPARATOR = "\n";
/** Default size for line wrapping.
*/
public static final int LINE_SIZE = 76;
/**
* This array is a lookup table that translates 6-bit positive integer
* index values into their "Base64 Alphabet" equivalents as specified
* in Table 1 of RFC 2045.
*/
private static final char intToBase64[] = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};
/**
* This array is a lookup table that translates unicode characters
* drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
* into their 6-bit positive integer equivalents. Characters that
* are not in the Base64 alphabet but fall within the bounds of the
* array are translated to -1.
*/
private static final byte base64ToInt[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
};
/** An encoder is an object, which is able to encode byte array
* in blocks of three bytes. Any such block is converted into an
* array of four bytes.
*/
public static abstract class Encoder {
private int num, numBytes;
private final char[] charBuffer;
private int charOffset;
private final int wrapSize;
private final int skipChars;
private final String sep;
private int lineChars = 0;
/** Creates a new instance.
* @param pBuffer The encoders buffer. The encoder will
* write to the buffer as long as possible. If the
* buffer is full or the end of data is signaled, then
* the method {@link #writeBuffer(char[], int, int)}
* will be invoked.
* @param pWrapSize A nonzero value indicates, that a line
* wrap should be performed after the given number of
* characters. The value must be a multiple of 4. Zero
* indicates, that no line wrap should be performed.
* @param pSep The eol sequence being used to terminate
* a line in case of line wraps. May be null, in which
* case the default value {@link Base64#LINE_SEPARATOR}
* is being used.
*/
protected Encoder(char[] pBuffer, int pWrapSize, String pSep) {
charBuffer = pBuffer;
sep = pSep == null ? null : Base64.LINE_SEPARATOR;
skipChars = pWrapSize == 0 ? 4 : 4 + sep.length();
wrapSize = skipChars == 4 ? 0 : pWrapSize;
if (wrapSize < 0 || wrapSize %4 > 0) {
throw new IllegalArgumentException("Illegal argument for wrap size: " + pWrapSize
+ "(Expected nonnegative multiple of 4)");
}
if (pBuffer.length < skipChars) {
throw new IllegalArgumentException("The buffer must contain at least " + skipChars
+ " characters, but has " + pBuffer.length);
}
}
/** Called for writing the buffer contents to the target.
* @param pChars The buffer being written.
* @param pOffset Offset of first character being written.
* @param pLen Number of characters being written.
* @throws IOException Writing to the destination failed.
*/
protected abstract void writeBuffer(char[] pChars, int pOffset, int pLen) throws IOException;
private void wrap() {
for (int j = 0; j < sep.length(); j++) {
charBuffer[charOffset++] = sep.charAt(j);
}
lineChars = 0;
}
/** Encodes the given byte array.
* @param pBuffer Byte array being encoded.
* @param pOffset Offset of first byte being encoded.
* @param pLen Number of bytes being encoded.
* @throws IOException Invoking the {@link #writeBuffer(char[],int,int)} method
* for writing the encoded data failed.
*/
public void write(byte[] pBuffer, int pOffset, int pLen) throws IOException {
for(int i = 0; i < pLen; i++) {
int b = pBuffer[pOffset++];
if (b < 0) { b += 256; }
num = (num << 8) + b;
if (++numBytes == 3) {
charBuffer[charOffset++] = intToBase64[num >> 18];
charBuffer[charOffset++] = intToBase64[(num >> 12) & 0x3f];
charBuffer[charOffset++] = intToBase64[(num >> 6) & 0x3f];
charBuffer[charOffset++] = intToBase64[num & 0x3f];
if (wrapSize > 0) {
lineChars += 4;
if (lineChars >= wrapSize) {
wrap();
}
}
num = 0;
numBytes = 0;
if (charOffset + skipChars > charBuffer.length) {
writeBuffer(charBuffer, 0, charOffset);
charOffset = 0;
}
}
}
}
/** Writes any currently buffered data to the destination.
* @throws IOException Invoking the {@link #writeBuffer(char[],int,int)}
* method for writing the encoded data failed.
*/
public void flush() throws IOException {
if (numBytes > 0) {
if (numBytes == 1) {
charBuffer[charOffset++] = intToBase64[num >> 2];
charBuffer[charOffset++] = intToBase64[(num << 4) & 0x3f];
charBuffer[charOffset++] = '=';
charBuffer[charOffset++] = '=';
} else {
charBuffer[charOffset++] = intToBase64[num >> 10];
charBuffer[charOffset++] = intToBase64[(num >> 4) & 0x3f];
charBuffer[charOffset++] = intToBase64[(num << 2) & 0x3f];
charBuffer[charOffset++] = '=';
}
lineChars += 4;
num = 0;
numBytes = 0;
}
if (wrapSize > 0 && lineChars > 0) {
wrap();
}
if (charOffset > 0) {
writeBuffer(charBuffer, 0, charOffset);
charOffset = 0;
}
}
}
/** An {@link OutputStream}, which is writing to the given
* {@link Encoder}.
*/
public static class EncoderOutputStream extends OutputStream {
private final Encoder encoder;
/** Creates a new instance, which is creating
* output using the given {@link Encoder}.
* @param pEncoder The base64 encoder being used.
*/
public EncoderOutputStream(Encoder pEncoder) {
encoder = pEncoder;
}
private final byte[] oneByte = new byte[1];
public void write(int b) throws IOException {
oneByte[0] = (byte) b;
encoder.write(oneByte, 0, 1);
}
public void write(byte[] pBuffer, int pOffset, int pLen) throws IOException {
encoder.write(pBuffer, pOffset, pLen);
}
public void close() throws IOException {
encoder.flush();
}
}
/** Returns an {@link OutputStream}, that encodes its input in Base64
* and writes it to the given {@link Writer}. If the Base64 stream
* ends, then the output streams {@link OutputStream#close()} method
* must be invoked. Note, that this will not close the
* target {@link Writer}!
* @param pWriter Target writer.
* @return An output stream, encoding its input in Base64 and writing
* the output to the writer pWriter
.
*/
public static OutputStream newEncoder(Writer pWriter) {
return newEncoder(pWriter, LINE_SIZE, LINE_SEPARATOR);
}
/** Returns an {@link OutputStream}, that encodes its input in Base64
* and writes it to the given {@link Writer}. If the Base64 stream
* ends, then the output streams {@link OutputStream#close()} method
* must be invoked. Note, that this will not close the
* target {@link Writer}!
* @param pWriter Target writer.
* @param pLineSize Size of one line in characters, must be a multiple
* of four. Zero indicates, that no line wrapping should occur.
* @param pSeparator Line separator or null, in which case the default value
* {@link #LINE_SEPARATOR} is used.
* @return An output stream, encoding its input in Base64 and writing
* the output to the writer pWriter
.
*/
public static OutputStream newEncoder(final Writer pWriter, int pLineSize, String pSeparator) {
final Encoder encoder = new Encoder(new char[4096], pLineSize, pSeparator){
protected void writeBuffer(char[] pBuffer, int pOffset, int pLen) throws IOException {
pWriter.write(pBuffer, pOffset, pLen);
}
};
return new EncoderOutputStream(encoder);
}
/** An {@link Encoder}, which is writing to a SAX content handler.
* This is typically used for embedding a base64 stream into an
* XML document.
*/
public static class SAXEncoder extends Encoder {
private final ContentHandler handler;
/** Creates a new instance.
* @param pBuffer The encoders buffer.
* @param pWrapSize A nonzero value indicates, that a line
* wrap should be performed after the given number of
* characters. The value must be a multiple of 4. Zero
* indicates, that no line wrap should be performed.
* @param pSep The eol sequence being used to terminate
* a line in case of line wraps. May be null, in which
* case the default value {@link Base64#LINE_SEPARATOR}
* is being used.
* @param pHandler The target handler.
*/
public SAXEncoder(char[] pBuffer, int pWrapSize, String pSep,
ContentHandler pHandler) {
super(pBuffer, pWrapSize, pSep);
handler = pHandler;
}
/** Writes to the content handler.
* @throws SAXIOException Writing to the content handler
* caused a SAXException.
*/
protected void writeBuffer(char[] pChars, int pOffset, int pLen) throws IOException {
try {
handler.characters(pChars, pOffset, pLen);
} catch (SAXException e) {
throw new SAXIOException(e);
}
}
}
/** Converts the given byte array into a base64 encoded character
* array.
* @param pBuffer The buffer being encoded.
* @param pOffset Offset in buffer, where to begin encoding.
* @param pLength Number of bytes being encoded.
* @return Character array of encoded bytes.
*/
public static String encode(byte[] pBuffer, int pOffset, int pLength) {
return encode(pBuffer, pOffset, pLength, LINE_SIZE, LINE_SEPARATOR);
}
/** Converts the given byte array into a base64 encoded character
* array.
* @param pBuffer The buffer being encoded.
* @param pOffset Offset in buffer, where to begin encoding.
* @param pLength Number of bytes being encoded.
* @param pLineSize Size of one line in characters, must be a multiple
* of four. Zero indicates, that no line wrapping should occur.
* @param pSeparator Line separator or null, in which case the default value
* {@link #LINE_SEPARATOR} is used.
* @return Character array of encoded bytes.
*/
public static String encode(byte[] pBuffer, int pOffset, int pLength,
int pLineSize, String pSeparator) {
StringWriter sw = new StringWriter();
OutputStream ostream = newEncoder(sw, pLineSize, pSeparator);
try {
ostream.write(pBuffer, pOffset, pLength);
ostream.close();
} catch (IOException e) {
throw new UndeclaredThrowableException(e);
}
return sw.toString();
}
/** Converts the given byte array into a base64 encoded character
* array with the line size {@link #LINE_SIZE} and the separator
* {@link #LINE_SEPARATOR}.
* @param pBuffer The buffer being encoded.
* @return Character array of encoded bytes.
*/
public static String encode(byte[] pBuffer) {
return encode(pBuffer, 0, pBuffer.length);
}
/** An encoder is an object, which is able to decode char arrays
* in blocks of four bytes. Any such block is converted into a
* array of three bytes.
*/
public static abstract class Decoder {
private final byte[] byteBuffer;
private int byteBufferOffset;
private int num, numBytes;
private int eofBytes;
/** Creates a new instance.
* @param pBufLen The decoders buffer size. The decoder will
* store up to this number of decoded bytes before invoking
* {@link #writeBuffer(byte[],int,int)}.
*/
protected Decoder(int pBufLen) {
byteBuffer = new byte[pBufLen];
}
/** Called for writing the decoded bytes to the destination.
* @param pBuffer The byte array being written.
* @param pOffset Offset of the first byte being written.
* @param pLen Number of bytes being written.
* @throws IOException Writing to the destination failed.
*/
protected abstract void writeBuffer(byte[] pBuffer, int pOffset, int pLen) throws IOException;
/** Converts the Base64 encoded character array.
* @param pData The character array being decoded.
* @param pOffset Offset of first character being decoded.
* @param pLen Number of characters being decoded.
* @throws DecodingException Decoding failed.
* @throws IOException An invocation of the {@link #writeBuffer(byte[],int,int)}
* method failed.
*/
public void write(char[] pData, int pOffset, int pLen) throws IOException {
for (int i = 0; i < pLen; i++) {
char c = pData[pOffset++];
if (Character.isWhitespace(c)) {
continue;
}
if (c == '=') {
++eofBytes;
num = num << 6;
switch(++numBytes) {
case 1:
case 2:
throw new DecodingException("Unexpected end of stream character (=)");
case 3:
// Wait for the next '='
break;
case 4:
byteBuffer[byteBufferOffset++] = (byte) (num >> 16);
if (eofBytes == 1) {
byteBuffer[byteBufferOffset++] = (byte) (num >> 8);
}
writeBuffer(byteBuffer, 0, byteBufferOffset);
byteBufferOffset = 0;
break;
case 5:
throw new DecodingException("Trailing garbage detected");
default:
throw new IllegalStateException("Invalid value for numBytes");
}
} else {
if (eofBytes > 0) {
throw new DecodingException("Base64 characters after end of stream character (=) detected.");
}
int result;
if (c >= 0 && c < base64ToInt.length) {
result = base64ToInt[c];
if (result >= 0) {
num = (num << 6) + result;
if (++numBytes == 4) {
byteBuffer[byteBufferOffset++] = (byte) (num >> 16);
byteBuffer[byteBufferOffset++] = (byte) ((num >> 8) & 0xff);
byteBuffer[byteBufferOffset++] = (byte) (num & 0xff);
if (byteBufferOffset + 3 > byteBuffer.length) {
writeBuffer(byteBuffer, 0, byteBufferOffset);
byteBufferOffset = 0;
}
num = 0;
numBytes = 0;
}
continue;
}
}
if (!Character.isWhitespace(c)) {
throw new DecodingException("Invalid Base64 character: " + (int) c);
}
}
}
}
/** Indicates, that no more data is being expected. Writes all currently
* buffered data to the destination by invoking {@link #writeBuffer(byte[],int,int)}.
* @throws DecodingException Decoding failed (Unexpected end of file).
* @throws IOException An invocation of the {@link #writeBuffer(byte[],int,int)} method failed.
*/
public void flush() throws IOException {
if (numBytes != 0 && numBytes != 4) {
throw new DecodingException("Unexpected end of file");
}
if (byteBufferOffset > 0) {
writeBuffer(byteBuffer, 0, byteBufferOffset);
byteBufferOffset = 0;
}
}
}
/** Returns a {@link Writer}, that decodes its Base64 encoded
* input and writes it to the given {@link OutputStream}.
* Note, that the writers {@link Writer#close()} method will
* not close the output stream pStream
!
* @param pStream Target output stream.
* @return An output stream, encoding its input in Base64 and writing
* the output to the writer pWriter
.
*/
public Writer newDecoder(final OutputStream pStream) {
return new Writer(){
private final Decoder decoder = new Decoder(1024){
protected void writeBuffer(byte[] pBytes, int pOffset, int pLen) throws IOException {
pStream.write(pBytes, pOffset, pLen);
}
};
public void close() throws IOException {
flush();
}
public void flush() throws IOException {
decoder.flush();
pStream.flush();
}
public void write(char[] cbuf, int off, int len) throws IOException {
decoder.write(cbuf, off, len);
}
};
}
/** Converts the given base64 encoded character buffer into a byte array.
* @param pBuffer The character buffer being decoded.
* @param pOffset Offset of first character being decoded.
* @param pLength Number of characters being decoded.
* @return Converted byte array
* @throws DecodingException The input character stream contained invalid data.
*/
public static byte[] decode(char[] pBuffer, int pOffset, int pLength) throws DecodingException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Decoder d = new Decoder(1024){
protected void writeBuffer(byte[] pBuf, int pOff, int pLen) throws IOException {
baos.write(pBuf, pOff, pLen);
}
};
try {
d.write(pBuffer, pOffset, pLength);
d.flush();
} catch (DecodingException e) {
throw e;
} catch (IOException e) {
throw new UndeclaredThrowableException(e);
}
return baos.toByteArray();
}
/** Converts the given base64 encoded character buffer into a byte array.
* @param pBuffer The character buffer being decoded.
* @return Converted byte array
* @throws DecodingException The input character stream contained invalid data.
*/
public static byte[] decode(char[] pBuffer) throws DecodingException {
return decode(pBuffer, 0, pBuffer.length);
}
/** Converts the given base64 encoded String into a byte array.
* @param pBuffer The string being decoded.
* @return Converted byte array
* @throws DecodingException The input character stream contained invalid data.
*/
public static byte[] decode(String pBuffer) throws DecodingException {
return decode(pBuffer.toCharArray());
}
}