Author: mwiederkehr Date: Thu Dec 18 05:33:52 2008 New Revision: 727723 URL: http://svn.apache.org/viewvc?rev=727723&view=rev Log: Added a Base64InputStream that uses block operations; fix for MIME4J-92
Added: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java (with props) Modified: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64InputStreamTest.java Added: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java?rev=727723&view=auto ============================================================================== --- james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java (added) +++ james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java Thu Dec 18 05:33:52 2008 @@ -0,0 +1,280 @@ +/**************************************************************** + * 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.decoder; + +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)"); + } +} Propchange: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64InputStream.java ------------------------------------------------------------------------------ svn:executable = * Modified: james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java?rev=727723&r1=727722&r2=727723&view=diff ============================================================================== --- james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java (original) +++ james/mime4j/trunk/src/main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java Thu Dec 18 05:33:52 2008 @@ -45,7 +45,7 @@ // 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 byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F', + static final byte[] BASE64_TABLE = { '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', Modified: james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64InputStreamTest.java URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64InputStreamTest.java?rev=727723&r1=727722&r2=727723&view=diff ============================================================================== --- james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64InputStreamTest.java (original) +++ james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64InputStreamTest.java Thu Dec 18 05:33:52 2008 @@ -24,7 +24,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; +import java.util.Random; +import org.apache.commons.io.output.NullOutputStream; import org.apache.james.mime4j.decoder.Base64InputStream; import org.apache.log4j.BasicConfigurator; @@ -155,6 +157,119 @@ } catch (IOException expected) { } } + + public void testRoundtripWithVariousBufferSizes() throws Exception { + byte[] data = new byte[3719]; + new Random(0).nextBytes(data); + + ByteArrayOutputStream eOut = new ByteArrayOutputStream(); + CodecUtil.encodeBase64(new ByteArrayInputStream(data), eOut); + byte[] encoded = eOut.toByteArray(); + + for (int bufferSize = 1; bufferSize <= 1009; bufferSize++) { + ByteArrayInputStream bis = new ByteArrayInputStream(encoded); + Base64InputStream decoder = new Base64InputStream(bis); + ByteArrayOutputStream dOut = new ByteArrayOutputStream(); + + final byte[] buffer = new byte[bufferSize]; + int inputLength; + while (-1 != (inputLength = decoder.read(buffer))) { + dOut.write(buffer, 0, inputLength); + } + + byte[] decoded = dOut.toByteArray(); + + assertEquals(data.length, decoded.length); + for (int i = 0; i < data.length; i++) { + assertEquals(data[i], decoded[i]); + } + } + } + + /** + * Tests {...@link InputStream#read()} + */ + public void testReadInt() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ==")); + Base64InputStream decoder = new Base64InputStream(bis); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + while (true) { + int x = decoder.read(); + if (x == -1) + break; + out.write(x); + } + + assertEquals("This is the plain text message!", toString(out + .toByteArray())); + } + + /** + * Tests {...@link InputStream#read(byte[], int, int)} with various offsets + */ + public void testReadOffset() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ==")); + Base64InputStream decoder = new Base64InputStream(bis); + + byte[] data = new byte[36]; + for (int i = 0;;) { + int bytes = decoder.read(data, i, 5); + if (bytes == -1) + break; + i += bytes; + } + + assertEquals("This is the plain text message!\0\0\0\0\0", + toString(data)); + } + + public void testStrictUnexpectedEof() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI")); + Base64InputStream decoder = new Base64InputStream(bis, true); + try { + CodecUtil.copy(decoder, new NullOutputStream()); + fail(); + } catch (IOException expected) { + assertTrue(expected.getMessage().toLowerCase().contains( + "end of file")); + } + } + + public void testLenientUnexpectedEof() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI")); + Base64InputStream decoder = new Base64InputStream(bis, false); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CodecUtil.copy(decoder, out); + assertEquals("This is the plain text message", toString(out + .toByteArray())); + } + + public void testStrictUnexpectedPad() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI=")); + Base64InputStream decoder = new Base64InputStream(bis, true); + try { + CodecUtil.copy(decoder, new NullOutputStream()); + fail(); + } catch (IOException expected) { + assertTrue(expected.getMessage().toLowerCase().contains("pad")); + } + } + + public void testLenientUnexpectedPad() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + fromString("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlI=")); + Base64InputStream decoder = new Base64InputStream(bis, false); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CodecUtil.copy(decoder, out); + assertEquals("This is the plain text message", toString(out + .toByteArray())); + } private byte[] read(InputStream is) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org