blob: 1bf40da3ffc3fcfd9ecf809c5ebf50a37344bdac [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.james.mime4j.codec;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Performs Base-64 decoding on an underlying stream.
*/
public class Base64InputStream extends InputStream {
private static Log log = LogFactory.getLog(Base64InputStream.class);
private static final int ENCODED_BUFFER_SIZE = 1536;
private static final int[] BASE64_DECODE = new int[256];
static {
for (int i = 0; i < 256; i++)
BASE64_DECODE[i] = -1;
for (int i = 0; i < Base64OutputStream.BASE64_TABLE.length; i++)
BASE64_DECODE[Base64OutputStream.BASE64_TABLE[i] & 0xff] = i;
}
private static final byte BASE64_PAD = '=';
private static final int EOF = -1;
private final byte[] singleByte = new byte[1];
private boolean strict;
private final InputStream in;
private boolean closed = false;
private final byte[] encoded = new byte[ENCODED_BUFFER_SIZE];
private int position = 0; // current index into encoded buffer
private int size = 0; // current size of encoded buffer
private final ByteQueue q = new ByteQueue();
private boolean eof; // end of file or pad character reached
public Base64InputStream(InputStream in) {
this(in, false);
}
public Base64InputStream(InputStream in, boolean strict) {
if (in == null)
throw new IllegalArgumentException();
this.in = in;
this.strict = strict;
}
@Override
public int read() throws IOException {
if (closed)
throw new IOException("Base64InputStream has been closed");
while (true) {
int bytes = read0(singleByte, 0, 1);
if (bytes == EOF)
return EOF;
if (bytes == 1)
return singleByte[0] & 0xff;
}
}
@Override
public int read(byte[] buffer) throws IOException {
if (closed)
throw new IOException("Base64InputStream has been closed");
if (buffer == null)
throw new NullPointerException();
if (buffer.length == 0)
return 0;
return read0(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
if (closed)
throw new IOException("Base64InputStream has been closed");
if (buffer == null)
throw new NullPointerException();
if (offset < 0 || length < 0 || offset + length > buffer.length)
throw new IndexOutOfBoundsException();
if (length == 0)
return 0;
return read0(buffer, offset, offset + length);
}
@Override
public void close() throws IOException {
if (closed)
return;
closed = true;
}
private int read0(final byte[] buffer, final int from, final int to)
throws IOException {
int index = from; // index into given buffer
// check if a previous invocation left decoded bytes in the queue
int qCount = q.count();
while (qCount-- > 0 && index < to) {
buffer[index++] = q.dequeue();
}
// eof or pad reached?
if (eof)
return index == from ? EOF : index - from;
// decode into given buffer
int data = 0; // holds decoded data; up to four sextets
int sextets = 0; // number of sextets
while (index < to) {
// make sure buffer not empty
while (position == size) {
int n = in.read(encoded, 0, encoded.length);
if (n == EOF) {
eof = true;
if (sextets != 0) {
// error in encoded data
handleUnexpectedEof(sextets);
}
return index == from ? EOF : index - from;
} else if (n > 0) {
position = 0;
size = n;
} else {
assert n == 0;
}
}
// decode buffer
while (position < size && index < to) {
int value = encoded[position++] & 0xff;
if (value == BASE64_PAD) {
index = decodePad(data, sextets, buffer, index, to);
return index - from;
}
int decoded = BASE64_DECODE[value];
if (decoded < 0) // -1: not a base64 char
continue;
data = (data << 6) | decoded;
sextets++;
if (sextets == 4) {
sextets = 0;
byte b1 = (byte) (data >>> 16);
byte b2 = (byte) (data >>> 8);
byte b3 = (byte) data;
if (index < to - 2) {
buffer[index++] = b1;
buffer[index++] = b2;
buffer[index++] = b3;
} else {
if (index < to - 1) {
buffer[index++] = b1;
buffer[index++] = b2;
q.enqueue(b3);
} else if (index < to) {
buffer[index++] = b1;
q.enqueue(b2);
q.enqueue(b3);
} else {
q.enqueue(b1);
q.enqueue(b2);
q.enqueue(b3);
}
assert index == to;
return to - from;
}
}
}
}
assert sextets == 0;
assert index == to;
return to - from;
}
private int decodePad(int data, int sextets, final byte[] buffer,
int index, final int end) throws IOException {
eof = true;
if (sextets == 2) {
// one byte encoded as "XY=="
byte b = (byte) (data >>> 4);
if (index < end) {
buffer[index++] = b;
} else {
q.enqueue(b);
}
} else if (sextets == 3) {
// two bytes encoded as "XYZ="
byte b1 = (byte) (data >>> 10);
byte b2 = (byte) ((data >>> 2) & 0xFF);
if (index < end - 1) {
buffer[index++] = b1;
buffer[index++] = b2;
} else if (index < end) {
buffer[index++] = b1;
q.enqueue(b2);
} else {
q.enqueue(b1);
q.enqueue(b2);
}
} else {
// error in encoded data
handleUnexpecedPad(sextets);
}
return index;
}
private void handleUnexpectedEof(int sextets) throws IOException {
if (strict)
throw new IOException("unexpected end of file");
else
log.warn("unexpected end of file; dropping " + sextets
+ " sextet(s)");
}
private void handleUnexpecedPad(int sextets) throws IOException {
if (strict)
throw new IOException("unexpected padding character");
else
log.warn("unexpected padding character; dropping " + sextets
+ " sextet(s)");
}
}