| /**************************************************************** |
| * 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) + "'"); |
| } |
| } |
| } |
| } |