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

Reply via email to