/**************************************************************** | |
* 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)"); | |
} | |
} |