You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by mw...@apache.org on 2008/12/12 14:06:55 UTC

svn commit: r725992 - in /james/mime4j/trunk/src: main/java/org/apache/james/mime4j/decoder/Base64OutputStream.java test/java/org/apache/james/mime4j/decoder/Base64OutputStreamTest.java

Author: mwiederkehr
Date: Fri Dec 12 05:06:54 2008
New Revision: 725992

URL: http://svn.apache.org/viewvc?rev=725992&view=rev
Log:
Faster Base64OutputStream for MIME4J-71

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/Base64OutputStreamTest.java

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=725992&r1=725991&r2=725992&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 Fri Dec 12 05:06:54 2008
@@ -22,382 +22,300 @@
 import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
- * This class is based on Base64 and Base64OutputStream code from Commons-Codec 1.4
- * 
- * Provides Base64 encoding and decoding as defined by RFC 2045.
- * 
+ * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite>
+ * from RFC 2045 <cite>Multipurpose Internet Mail Extensions (MIME) Part One:
+ * Format of Internet Message Bodies</cite> by Freed and Borenstein.
  * <p>
- * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
- * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
- * </p>
+ * Code is based on Base64 and Base64OutputStream code from Commons-Codec 1.4.
  * 
  * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
- * @author Apache Software Foundation
- * @since 1.0-dev
- * @version $Id$
  */
 public class Base64OutputStream extends FilterOutputStream {
 
-    /**
-     * Chunk size per RFC 2045 section 6.8.
-     * 
-     * <p>
-     * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
-     * equal signs.
-     * </p>
-     * 
-     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
-     */
-    static final int CHUNK_SIZE = 76;
-
-    /**
-     * Chunk separator per RFC 2045 section 2.1.
-     * 
-     * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
-     */
-    static final byte[] CHUNK_SEPARATOR = {'\r','\n'};
-
-    /**
-     * 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.
-     *
-     * Thanks to "commons" project in ws.apache.org for this code. 
-     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
-     */
-    private static final byte[] intToBase64 = {
-            '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', 't', 'u', 'v', 'w', 'x', 'y', 'z',
-            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
-    };
+    // Default line length per RFC 2045 section 6.8.
+    private static final int DEFAULT_LINE_LENGTH = 76;
 
-    /**
-     * Byte used to pad output.
-     */
-    private static final byte PAD = '=';
+    // CRLF line separator per RFC 2045 section 2.1.
+    private static final byte[] CRLF_SEPARATOR = { '\r', '\n' };
 
-    /**
-     * This array is a lookup table that translates unicode characters
-     * drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
-     * into their 6-bit positive integer equivalents.  Characters that
-     * are not in the Base64 alphabet but fall within the bounds of the
-     * array are translated to -1.
-     *
-     * Thanks to "commons" project in ws.apache.org for this code.
-     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ 
-     */
-    private static final byte[] base64ToInt = {
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-            -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
-            55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
-            5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
-            24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
-            35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
-    };
+    // 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',
+            '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',
+            't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
+            '6', '7', '8', '9', '+', '/' };
+
+    // Byte used to pad output.
+    private static final byte BASE64_PAD = '=';
+
+    // This set contains all base64 characters including the pad character. Used
+    // solely to check if a line separator contains any of these characters.
+    private static final Set<Byte> BASE64_CHARS = new HashSet<Byte>();
+
+    static {
+        for (byte b : BASE64_TABLE) {
+            BASE64_CHARS.add(b);
+        }
+        BASE64_CHARS.add(BASE64_PAD);
+    }
 
-    /** Mask used to extract 6 bits, used when encoding */
+    // Mask used to extract 6 bits
     private static final int MASK_6BITS = 0x3f;
 
-    // The static final fields above are used for the original static byte[] methods on Base64.
-    // The private member fields below are used with the new streaming approach, which requires
-    // some state be preserved between calls of encode() and decode().
+    private static final int ENCODED_BUFFER_SIZE = 2048;
 
+    private final byte[] singleByte = new byte[1];
 
-    /**
-     * Line length for encoding.  Not used when decoding.  A value of zero or less implies
-     * no chunking of the base64 encoded data.
-     */
     private final int lineLength;
-
-    /**
-     * Line separator for encoding.  Not used when decoding.  Only used if lineLength > 0.
-     */
     private final byte[] lineSeparator;
 
-    /**
-     * Convenience variable to help us determine when our buffer is going to run out of
-     * room and needs resizing.  <code>encodeSize = 4 + lineSeparator.length;</code>
-     */
-    private final int encodeSize;
-
-    /**
-     * Buffer for streaming. 
-     */
-    private byte[] buf = new byte[1024];
-
-    /**
-     * Position where next character should be written in the buffer.
-     */
-    private int pos;
+    private boolean closed = false;
 
-    /**
-     * Variable tracks how many characters have been written to the current line.
-     * Only used when encoding.  We use it to make sure each encoded line never
-     * goes beyond lineLength (if lineLength > 0).
-     */
-    private int currentLinePos;
+    private final byte[] encoded;
+    private int position = 0;
 
-    /**
-     * Writes to the buffer only occur after every 3 reads when encoding, an
-     * every 4 reads when decoding.  This variable helps track that.
-     */
-    private int modulus;
+    private int data = 0;
+    private int modulus = 0;
 
-    /**
-     * Boolean flag to indicate that this Base64OutputStream has been closed.
-     * Once closed, this Base64 object becomes useless, and must be thrown away.
-     */
-    private boolean closed = false;
-        
-    /**
-     * Place holder for the 3 bytes we're dealing with for our base64 logic.
-     * Bitwise operations store and extract the base64 encoding or decoding from
-     * this variable.
-     */
-    private int x;
+    private int linePosition = 0;
 
     /**
-     * Default constructor:  lineLength is 76, and the lineSeparator is CRLF
-     * when encoding, and all forms can be decoded.
+     * Creates a <code>Base64OutputStream</code> that writes the encoded data
+     * to the given output stream using the default line length (76) and line
+     * separator (CRLF).
+     * 
+     * @param out
+     *            underlying output stream.
      */
-    public Base64OutputStream(OutputStream os) {
-        this(os, CHUNK_SIZE, CHUNK_SEPARATOR);
+    public Base64OutputStream(OutputStream out) {
+        this(out, DEFAULT_LINE_LENGTH, CRLF_SEPARATOR);
     }
 
     /**
+     * Creates a <code>Base64OutputStream</code> that writes the encoded data
+     * to the given output stream using the given line length and the default
+     * line separator (CRLF).
      * <p>
-     * Consumer can use this constructor to choose a different lineLength
-     * when encoding (lineSeparator is still CRLF).  All forms of data can
-     * be decoded.
-     * </p><p>
-     * Note:  lineLengths that aren't multiples of 4 will still essentially
-     * end up being multiples of 4 in the encoded data.
-     * </p>
-     *
-     * @param lineLength each line of encoded data will be at most this long
-     * (rounded up to nearest multiple of 4). 
-     * If lineLength <= 0, then the output will not be divided into lines (chunks).  
-     * Ignored when decoding.
+     * The given line length will be rounded up to the nearest multiple of 4. If
+     * the line length is zero then the output will not be split into lines.
+     * 
+     * @param out
+     *            underlying output stream.
+     * @param lineLength
+     *            desired line length.
      */
-    public Base64OutputStream(OutputStream os, int lineLength) {
-        this(os, lineLength, CHUNK_SEPARATOR);
+    public Base64OutputStream(OutputStream out, int lineLength) {
+        this(out, lineLength, CRLF_SEPARATOR);
     }
 
     /**
+     * Creates a <code>Base64OutputStream</code> that writes the encoded data
+     * to the given output stream using the given line length and line
+     * separator.
      * <p>
-     * Consumer can use this constructor to choose a different lineLength
-     * and lineSeparator when encoding.  All forms of data can
-     * be decoded.
-     * </p><p>
-     * Note:  lineLengths that aren't multiples of 4 will still essentially
-     * end up being multiples of 4 in the encoded data.
-     * </p>
-     * @param lineLength    Each line of encoded data will be at most this long
-     *                      (rounded up to nearest multiple of 4).  Ignored when decoding.
-     *                      If <= 0, then output will not be divided into lines (chunks).
-     * @param lineSeparator Each line of encoded data will end with this
-     *                      sequence of bytes.
-     *                      If lineLength <= 0, then the lineSeparator is not used.
-     * @throws IllegalArgumentException The provided lineSeparator included
-     *                                  some base64 characters.  That's not going to work!
-     */
-    public Base64OutputStream(OutputStream os, int lineLength, byte[] lineSeparator) {
-        super(os);
+     * The given line length will be rounded up to the nearest multiple of 4. If
+     * the line length is zero then the output will not be split into lines and
+     * the line separator is ignored.
+     * <p>
+     * The line separator must not include characters from the BASE64 alphabet
+     * (including the padding character <code>=</code>).
+     * 
+     * @param out
+     *            underlying output stream.
+     * @param lineLength
+     *            desired line length.
+     * @param lineSeparator
+     *            line separator to use.
+     */
+    public Base64OutputStream(OutputStream out, int lineLength,
+            byte[] lineSeparator) {
+        super(out);
+
+        if (out == null)
+            throw new IllegalArgumentException();
+        if (lineLength < 0)
+            throw new IllegalArgumentException();
+        checkLineSeparator(lineSeparator);
+
         this.lineLength = lineLength;
         this.lineSeparator = new byte[lineSeparator.length];
-        System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
-        if (lineLength > 0) {
-            this.encodeSize = 4 + lineSeparator.length;
-        } else {
-            this.encodeSize = 4;
-        }
-        if (containsBase64Byte(lineSeparator)) {
-            String sep;
-            try {
-                sep = new String(lineSeparator, "UTF-8");
-            } catch (UnsupportedEncodingException uee) {
-                sep = new String(lineSeparator);
-            }
-            throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]");
-        }
-    }
+        System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,
+                lineSeparator.length);
 
-    /** Doubles our buffer. */
-    private void resizeBuf() {
-        byte[] b = new byte[buf.length * 2];
-        System.arraycopy(buf, 0, b, 0, buf.length);
-        buf = b;
+        this.encoded = new byte[ENCODED_BUFFER_SIZE];
     }
 
-    /**
-     * Returns whether or not the <code>octet</code> is in the base 64 alphabet.
-     * 
-     * @param octet
-     *            The value to test
-     * @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
-     */
-    public static boolean isBase64(byte octet) {
-        return octet == PAD || (octet >= 0 && octet < base64ToInt.length && base64ToInt[octet] != -1);
-    }
+    @Override
+    public final void write(final int b) throws IOException {
+        if (closed)
+            throw new IOException("Base64OutputStream has been closed");
 
-    /*
-     * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
-     * 
-     * @param arrayOctet
-     *            byte array to test
-     * @return <code>true</code> if any byte is a valid character in the Base64 alphabet; false herwise
-     */
-    private static boolean containsBase64Byte(byte[] arrayOctet) {
-        for (byte octet : arrayOctet) {
-            if (isBase64(octet)) {
-                return true;
-            }
-        }
-        return false;
+        singleByte[0] = (byte) b;
+        write0(singleByte, 0, 1);
     }
 
-    // Implementation of the Encoder Interface
+    @Override
+    public final void write(final byte[] buffer) throws IOException {
+        if (closed)
+            throw new IOException("Base64OutputStream has been closed");
 
-    private final byte[] singleByte = new byte[1];
+        if (buffer == null)
+            throw new NullPointerException();
 
-    /**
-     * Writes the specified <code>byte</code> to this output stream.
-     */
-    @Override
-    public void write(int i) throws IOException {
-        singleByte[0] = (byte) i;
-        write(singleByte, 0, 1);
+        if (buffer.length == 0)
+            return;
+
+        write0(buffer, 0, buffer.length);
     }
 
-    
-    /**
-     * Writes <code>len</code> bytes from the specified
-     * <code>b</code> array starting at <code>offset</code> to
-     * this output stream.
-     *
-     * @param b source byte array
-     * @param offset where to start reading the bytes
-     * @param len maximum number of bytes to write
-     * 
-     * @throws IOException if an I/O error occurs.
-     * @throws NullPointerException if the byte array parameter is null
-     * @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
-     */
     @Override
-    public void write(byte b[], int offset, int len) throws IOException {
-        if (closed) {
-        	throw new IOException("Base64OutputStream has been closed");
-        }
-        if (b == null) {
+    public final void write(final byte[] buffer, final int offset,
+            final int length) throws IOException {
+        if (closed)
+            throw new IOException("Base64OutputStream has been closed");
+
+        if (buffer == null)
             throw new NullPointerException();
-        } else if (len < 0) {
-            // len < 0 is how we're informed of EOF in the underlying data we're
-            // encoding.
-            closed = true;
-            if (buf.length - pos < encodeSize) {
-                resizeBuf();
-            }
-            switch (modulus) {
-                case 1:
-                    buf[pos++] = intToBase64[(x >> 2) & MASK_6BITS];
-                    buf[pos++] = intToBase64[(x << 4) & MASK_6BITS];
-                    buf[pos++] = PAD;
-                    buf[pos++] = PAD;
-                    break;
-
-                case 2:
-                    buf[pos++] = intToBase64[(x >> 10) & MASK_6BITS];
-                    buf[pos++] = intToBase64[(x >> 4) & MASK_6BITS];
-                    buf[pos++] = intToBase64[(x << 2) & MASK_6BITS];
-                    buf[pos++] = PAD;
-                    break;
-            }
-            if (lineLength > 0) {
-                System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
-                pos += lineSeparator.length;
-            }
-        } else if (offset < 0 || len < 0 || offset + len < 0) {
-            throw new IndexOutOfBoundsException();
-        } else if (offset > b.length || offset + len > b.length) {
+
+        if (offset < 0 || length < 0 || offset + length > buffer.length)
             throw new IndexOutOfBoundsException();
-        } else if (len > 0) {
-            for (int i = 0; i < len; i++) {
-                if (buf.length - pos < encodeSize) {
-                    resizeBuf();
-                }
-                modulus = (++modulus) % 3;
-                int bc = b[offset++];
-                if (bc < 0) { bc += 256; }
-                x = (x << 8) + bc;
-                if (0 == modulus) {
-                    buf[pos++] = intToBase64[(x >> 18) & MASK_6BITS];
-                    buf[pos++] = intToBase64[(x >> 12) & MASK_6BITS];
-                    buf[pos++] = intToBase64[(x >> 6) & MASK_6BITS];
-                    buf[pos++] = intToBase64[x & MASK_6BITS];
-                    currentLinePos += 4;
-                    if (lineLength > 0 && lineLength <= currentLinePos) {
-                        System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
-                        pos += lineSeparator.length;
-                        currentLinePos = 0;
-                    }
-                }
-            }
-        }
-        flushBuffer();
-    }
-    
-    /**
-     * Flushes this output stream and forces any buffered output bytes
-     * to be written out to the stream.  If propogate is true, the wrapped
-     * stream will also be flushed.
-     *
-     * @param propogate boolean flag to indicate whether the wrapped
-     *                  OutputStream should also be flushed.
-     * @throws IOException if an I/O error occurs.
-     */
-    private void flushBuffer() throws IOException {
-        if (pos > 0) {
-            out.write(buf, 0, pos);
-            // buf = null;
-            pos = 0;
-        }
+
+        if (length == 0)
+            return;
+
+        write0(buffer, offset, offset + length);
     }
 
-    /**
-     * Flushes this output stream and forces any buffered output bytes
-     * to be written out to the stream.
-     *
-     * @throws IOException if an I/O error occurs.
-     */
     @Override
     public void flush() throws IOException {
-        flushBuffer();
-        out.flush();
+        if (closed)
+            throw new IOException("Base64OutputStream has been closed");
+
+        flush0();
     }
-    
-    /**
-     * Terminates the BASE64 coded content and flushes the internal buffers. This method does
-     * NOT close the underlying output stream.
-     */
+
     @Override
     public void close() throws IOException {
-        if (closed) {
+        if (closed)
             return;
+
+        closed = true;
+        close0();
+    }
+
+    private void write0(final byte[] buffer, final int from, final int to)
+            throws IOException {
+        for (int i = from; i < to; i++) {
+            data = (data << 8) | (buffer[i] & 0xff);
+
+            if (++modulus == 3) {
+                modulus = 0;
+
+                // write line separator if necessary
+
+                if (lineLength > 0 && linePosition >= lineLength) {
+                    // writeLineSeparator() inlined for performance reasons
+
+                    linePosition = 0;
+
+                    if (encoded.length - position < lineSeparator.length)
+                        flush0();
+
+                    for (byte ls : lineSeparator)
+                        encoded[position++] = ls;
+                }
+
+                // encode data into 4 bytes
+
+                if (encoded.length - position < 4)
+                    flush0();
+
+                encoded[position++] = BASE64_TABLE[(data >> 18) & MASK_6BITS];
+                encoded[position++] = BASE64_TABLE[(data >> 12) & MASK_6BITS];
+                encoded[position++] = BASE64_TABLE[(data >> 6) & MASK_6BITS];
+                encoded[position++] = BASE64_TABLE[data & MASK_6BITS];
+
+                linePosition += 4;
+            }
+        }
+    }
+
+    private void flush0() throws IOException {
+        if (position > 0) {
+            out.write(encoded, 0, position);
+            position = 0;
+        }
+    }
+
+    private void close0() throws IOException {
+        if (modulus != 0)
+            writePad();
+
+        // write line separator at the end of the encoded data
+
+        if (lineLength > 0 && linePosition > 0) {
+            writeLineSeparator();
         }
 
-        try {
-            // Notify encoder of EOF (-1).
-            write(singleByte, 0, -1);
-            flush();
-        } finally {
-            closed = true;
+        flush0();
+    }
+
+    private void writePad() throws IOException {
+        // write line separator if necessary
+
+        if (lineLength > 0 && linePosition >= lineLength) {
+            writeLineSeparator();
         }
+
+        // encode data into 4 bytes
+
+        if (encoded.length - position < 4)
+            flush0();
+
+        if (modulus == 1) {
+            encoded[position++] = BASE64_TABLE[(data >> 2) & MASK_6BITS];
+            encoded[position++] = BASE64_TABLE[(data << 4) & MASK_6BITS];
+            encoded[position++] = BASE64_PAD;
+            encoded[position++] = BASE64_PAD;
+        } else {
+            assert modulus == 2;
+            encoded[position++] = BASE64_TABLE[(data >> 10) & MASK_6BITS];
+            encoded[position++] = BASE64_TABLE[(data >> 4) & MASK_6BITS];
+            encoded[position++] = BASE64_TABLE[(data << 2) & MASK_6BITS];
+            encoded[position++] = BASE64_PAD;
+        }
+
+        linePosition += 4;
     }
 
+    private void writeLineSeparator() throws IOException {
+        linePosition = 0;
+
+        if (encoded.length - position < lineSeparator.length)
+            flush0();
+
+        for (byte ls : lineSeparator)
+            encoded[position++] = ls;
+    }
+
+    private void checkLineSeparator(byte[] lineSeparator) {
+        if (lineSeparator.length > ENCODED_BUFFER_SIZE)
+            throw new IllegalArgumentException("line separator length exceeds "
+                    + ENCODED_BUFFER_SIZE);
+
+        for (byte b : lineSeparator) {
+            if (BASE64_CHARS.contains(b)) {
+                throw new IllegalArgumentException(
+                        "line separator must not contain base64 character '"
+                                + (char) (b & 0xff) + "'");
+            }
+        }
+    }
 }

Modified: james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64OutputStreamTest.java
URL: http://svn.apache.org/viewvc/james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64OutputStreamTest.java?rev=725992&r1=725991&r2=725992&view=diff
==============================================================================
--- james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64OutputStreamTest.java (original)
+++ james/mime4j/trunk/src/test/java/org/apache/james/mime4j/decoder/Base64OutputStreamTest.java Fri Dec 12 05:06:54 2008
@@ -21,6 +21,7 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 
 import junit.framework.TestCase;
@@ -62,6 +63,139 @@
         bos.write('a');
         assertEquals("VGhpcyBpcyB0aGUgcGxhaW4gdGV4dCBtZXNzYWdlIQ==\r\nyada", toString(bos.toByteArray()));
     }
+
+    public void testNoLineSeparators() throws IOException {
+        assertEquals("", encodeNoLs(""));
+        assertEquals("YQ==", encodeNoLs("a"));
+        assertEquals("YWI=", encodeNoLs("ab"));
+        assertEquals("YWJj", encodeNoLs("abc"));
+        assertEquals("YWJjZA==", encodeNoLs("abcd"));
+        assertEquals("YWJjZGU=", encodeNoLs("abcde"));
+        assertEquals("YWJjZGVm", encodeNoLs("abcdef"));
+        assertEquals("YWJjZGVmZw==", encodeNoLs("abcdefg"));
+        assertEquals("YWJjZGVmZ2g=", encodeNoLs("abcdefgh"));
+        assertEquals("YWJjZGVmZ2hp", encodeNoLs("abcdefghi"));
+        assertEquals("DQoMCQ==", encodeNoLs("\r\n\f\t"));
+        assertEquals("LT0/VGhhdCdzIGEgdGVzdD89LQ==",
+                encodeNoLs("-=?That's a test?=-"));
+    }
+
+    public void testLineSeparators() throws IOException {
+        assertEquals("", encodeLs(""));
+        assertEquals("YQ==\r\n", encodeLs("a"));
+        assertEquals("YWJjZA==\r\n", encodeLs("abcd"));
+        assertEquals("YWJjZGVmZw==\r\n", encodeLs("abcdefg"));
+        assertEquals("YWJjZGVmZ2g=\r\n", encodeLs("abcdefgh"));
+        assertEquals("YWJjZGVmZ2hp\r\n", encodeLs("abcdefghi"));
+        assertEquals("YWJjZGVmZ2hp\r\nag==\r\n", encodeLs("abcdefghij"));
+        assertEquals("YWJjZGVmZ2hp\r\nams=\r\n", encodeLs("abcdefghijk"));
+        assertEquals("YWJjZGVmZ2hp\r\namts\r\n", encodeLs("abcdefghijkl"));
+    }
+
+    /**
+     * tests {@link OutputStream#write(int)}
+     */
+    public void testWriteInt() throws IOException {
+        byte[] bytes = fromString("123456789012345678901234567890123456789012"
+                + "3456789012345678901234567890123456789012345678901234567890"
+                + "123456789012345");
+
+        ByteArrayOutputStream b = new ByteArrayOutputStream();
+        Base64OutputStream out = new Base64OutputStream(b);
+
+        for (byte element : bytes)
+            out.write(element);
+
+        out.close();
+
+        String actual = new String(b.toByteArray(), "US-ASCII");
+
+        String expected = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nz"
+                + "g5MDEyMzQ1Njc4OTAxMjM0NTY3\r\nODkwMTIzNDU2Nzg5MDEyMzQ1Njc4"
+                + "OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\nNQ==\r"
+                + "\n";
+
+        assertEquals(expected, actual);
+    }
+
+    /**
+     * tests {@link OutputStream#write(byte[], int, int)} with various offsets
+     */
+    public void testWriteOffset() throws IOException {
+        byte[] bytes = fromString("123456789012345678901234567890123456789012"
+                + "3456789012345678901234567890123456789012345678901234567890"
+                + "123456789012345");
+
+        ByteArrayOutputStream b = new ByteArrayOutputStream();
+        Base64OutputStream out = new Base64OutputStream(b);
+
+        for (int offset = 0; offset < bytes.length; offset += 2) {
+            int len = Math.min(2, bytes.length - offset);
+            out.write(bytes, offset, len);
+        }
+
+        out.close();
+
+        String actual = new String(b.toByteArray(), "US-ASCII");
+
+        String expected = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nz"
+                + "g5MDEyMzQ1Njc4OTAxMjM0NTY3\r\nODkwMTIzNDU2Nzg5MDEyMzQ1Njc4"
+                + "OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\nNQ==\r"
+                + "\n";
+
+        assertEquals(expected, actual);
+    }
+
+    /**
+     * tests {@link OutputStream#flush()} while writing
+     */
+    public void testWriteFlush() throws IOException {
+        byte[] bytes = fromString("123456789012345678901234567890123456789012"
+                + "3456789012345678901234567890123456789012345678901234567890"
+                + "123456789012345");
+
+        ByteArrayOutputStream b = new ByteArrayOutputStream();
+        Base64OutputStream out = new Base64OutputStream(b);
+
+        for (int offset = 0; offset < bytes.length; offset += 7) {
+            int len = Math.min(7, bytes.length - offset);
+            out.write(bytes, offset, len);
+            out.flush();
+        }
+
+        out.close();
+
+        String actual = new String(b.toByteArray(), "US-ASCII");
+
+        String expected = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nz"
+                + "g5MDEyMzQ1Njc4OTAxMjM0NTY3\r\nODkwMTIzNDU2Nzg5MDEyMzQ1Njc4"
+                + "OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\nNQ==\r"
+                + "\n";
+
+        assertEquals(expected, actual);
+    }
+
+    private String encodeNoLs(String str) throws IOException {
+        return encode(str, 0, new byte[] {});
+    }
+
+    private String encodeLs(String str) throws IOException {
+        return encode(str, 12, new byte[] { '\r', '\n' });
+    }
+
+    private String encode(String str, int lineLength, byte[] lineSeparator)
+            throws IOException {
+        byte[] bytes = fromString(str);
+
+        ByteArrayOutputStream b = new ByteArrayOutputStream();
+        Base64OutputStream out = new Base64OutputStream(b, lineLength,
+                lineSeparator);
+
+        out.write(bytes);
+        out.close();
+
+        return toString(b.toByteArray());
+    }
         
     private byte[] fromString(String s) {
         try {



---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org