/**************************************************************** * 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.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; import java.util.Set; /** * 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> */ public class Base64OutputStream extends FilterOutputStream { // Default line length per RFC 2045 section 6.8. private static final int DEFAULT_LINE_LENGTH = 76; // 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 6-bit positive integer index // values into their "Base64 Alphabet" equivalents as specified in Table 1 // of RFC 2045. 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 private static final int MASK_6BITS = 0x3f; private static final int ENCODED_BUFFER_SIZE = 2048; private final byte[] singleByte = new byte[1]; private final int lineLength; private final byte[] lineSeparator; private boolean closed = false; private final byte[] encoded; private int position = 0; private int data = 0; private int modulus = 0; private int linePosition = 0; /** * 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 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> * 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 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> * 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); this.encoded = new byte[ENCODED_BUFFER_SIZE]; } @Override public final void write(final int b) throws IOException { if (closed) throw new IOException("Base64OutputStream has been closed"); singleByte[0] = (byte) b; write0(singleByte, 0, 1); } @Override public final void write(final byte[] buffer) throws IOException { if (closed) throw new IOException("Base64OutputStream has been closed"); if (buffer == null) throw new NullPointerException(); if (buffer.length == 0) return; write0(buffer, 0, buffer.length); } @Override 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(); if (offset < 0 || length < 0 || offset + length > buffer.length) throw new IndexOutOfBoundsException(); if (length == 0) return; write0(buffer, offset, offset + length); } @Override public void flush() throws IOException { if (closed) throw new IOException("Base64OutputStream has been closed"); flush0(); } @Override public void close() throws IOException { 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(); } 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) + "'"); } } } }