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: [email protected]
For additional commands, e-mail: [email protected]