001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mime4j.codec;
021
022import java.io.FilterOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.util.HashSet;
026import java.util.Set;
027
028/**
029 * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite>
030 * from RFC 2045 <cite>Multipurpose Internet Mail Extensions (MIME) Part One:
031 * Format of Internet Message Bodies</cite> by Freed and Borenstein.
032 * <p>
033 * Code is based on Base64 and Base64OutputStream code from Commons-Codec 1.4.
034 *
035 * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
036 */
037public class Base64OutputStream extends FilterOutputStream {
038
039    // Default line length per RFC 2045 section 6.8.
040    private static final int DEFAULT_LINE_LENGTH = 76;
041
042    // CRLF line separator per RFC 2045 section 2.1.
043    private static final byte[] CRLF_SEPARATOR = { '\r', '\n' };
044
045    // This array is a lookup table that translates 6-bit positive integer index
046    // values into their "Base64 Alphabet" equivalents as specified in Table 1
047    // of RFC 2045.
048    static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F',
049            'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
050            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
051            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
052            't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
053            '6', '7', '8', '9', '+', '/' };
054
055    // Byte used to pad output.
056    private static final byte BASE64_PAD = '=';
057
058    // This set contains all base64 characters including the pad character. Used
059    // solely to check if a line separator contains any of these characters.
060    private static final Set<Byte> BASE64_CHARS = new HashSet<Byte>();
061
062    static {
063        for (byte b : BASE64_TABLE) {
064            BASE64_CHARS.add(b);
065        }
066        BASE64_CHARS.add(BASE64_PAD);
067    }
068
069    // Mask used to extract 6 bits
070    private static final int MASK_6BITS = 0x3f;
071
072    private static final int ENCODED_BUFFER_SIZE = 2048;
073
074    private final byte[] singleByte = new byte[1];
075
076    private final int lineLength;
077    private final byte[] lineSeparator;
078
079    private boolean closed = false;
080
081    private final byte[] encoded;
082    private int position = 0;
083
084    private int data = 0;
085    private int modulus = 0;
086
087    private int linePosition = 0;
088
089    /**
090     * Creates a <code>Base64OutputStream</code> that writes the encoded data
091     * to the given output stream using the default line length (76) and line
092     * separator (CRLF).
093     *
094     * @param out
095     *            underlying output stream.
096     */
097    public Base64OutputStream(OutputStream out) {
098        this(out, DEFAULT_LINE_LENGTH, CRLF_SEPARATOR);
099    }
100
101    /**
102     * Creates a <code>Base64OutputStream</code> that writes the encoded data
103     * to the given output stream using the given line length and the default
104     * line separator (CRLF).
105     * <p>
106     * The given line length will be rounded up to the nearest multiple of 4. If
107     * the line length is zero then the output will not be split into lines.
108     *
109     * @param out
110     *            underlying output stream.
111     * @param lineLength
112     *            desired line length.
113     */
114    public Base64OutputStream(OutputStream out, int lineLength) {
115        this(out, lineLength, CRLF_SEPARATOR);
116    }
117
118    /**
119     * Creates a <code>Base64OutputStream</code> that writes the encoded data
120     * to the given output stream using the given line length and line
121     * separator.
122     * <p>
123     * The given line length will be rounded up to the nearest multiple of 4. If
124     * the line length is zero then the output will not be split into lines and
125     * the line separator is ignored.
126     * <p>
127     * The line separator must not include characters from the BASE64 alphabet
128     * (including the padding character <code>=</code>).
129     *
130     * @param out
131     *            underlying output stream.
132     * @param lineLength
133     *            desired line length.
134     * @param lineSeparator
135     *            line separator to use.
136     */
137    public Base64OutputStream(OutputStream out, int lineLength,
138            byte[] lineSeparator) {
139        super(out);
140
141        if (out == null)
142            throw new IllegalArgumentException();
143        if (lineLength < 0)
144            throw new IllegalArgumentException();
145        checkLineSeparator(lineSeparator);
146
147        this.lineLength = lineLength;
148        this.lineSeparator = new byte[lineSeparator.length];
149        System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,
150                lineSeparator.length);
151
152        this.encoded = new byte[ENCODED_BUFFER_SIZE];
153    }
154
155    @Override
156    public final void write(final int b) throws IOException {
157        if (closed)
158            throw new IOException("Base64OutputStream has been closed");
159
160        singleByte[0] = (byte) b;
161        write0(singleByte, 0, 1);
162    }
163
164    @Override
165    public final void write(final byte[] buffer) throws IOException {
166        if (closed)
167            throw new IOException("Base64OutputStream has been closed");
168
169        if (buffer == null)
170            throw new NullPointerException();
171
172        if (buffer.length == 0)
173            return;
174
175        write0(buffer, 0, buffer.length);
176    }
177
178    @Override
179    public final void write(final byte[] buffer, final int offset,
180            final int length) throws IOException {
181        if (closed)
182            throw new IOException("Base64OutputStream has been closed");
183
184        if (buffer == null)
185            throw new NullPointerException();
186
187        if (offset < 0 || length < 0 || offset + length > buffer.length)
188            throw new IndexOutOfBoundsException();
189
190        if (length == 0)
191            return;
192
193        write0(buffer, offset, offset + length);
194    }
195
196    @Override
197    public void flush() throws IOException {
198        if (closed)
199            throw new IOException("Base64OutputStream has been closed");
200
201        flush0();
202    }
203
204    @Override
205    public void close() throws IOException {
206        if (closed)
207            return;
208
209        closed = true;
210        close0();
211    }
212
213    private void write0(final byte[] buffer, final int from, final int to)
214            throws IOException {
215        for (int i = from; i < to; i++) {
216            data = (data << 8) | (buffer[i] & 0xff);
217
218            if (++modulus == 3) {
219                modulus = 0;
220
221                // write line separator if necessary
222
223                if (lineLength > 0 && linePosition >= lineLength) {
224                    // writeLineSeparator() inlined for performance reasons
225
226                    linePosition = 0;
227
228                    if (encoded.length - position < lineSeparator.length)
229                        flush0();
230
231                    for (byte ls : lineSeparator)
232                        encoded[position++] = ls;
233                }
234
235                // encode data into 4 bytes
236
237                if (encoded.length - position < 4)
238                    flush0();
239
240                encoded[position++] = BASE64_TABLE[(data >> 18) & MASK_6BITS];
241                encoded[position++] = BASE64_TABLE[(data >> 12) & MASK_6BITS];
242                encoded[position++] = BASE64_TABLE[(data >> 6) & MASK_6BITS];
243                encoded[position++] = BASE64_TABLE[data & MASK_6BITS];
244
245                linePosition += 4;
246            }
247        }
248    }
249
250    private void flush0() throws IOException {
251        if (position > 0) {
252            out.write(encoded, 0, position);
253            position = 0;
254        }
255    }
256
257    private void close0() throws IOException {
258        if (modulus != 0)
259            writePad();
260
261        // write line separator at the end of the encoded data
262
263        if (lineLength > 0 && linePosition > 0) {
264            writeLineSeparator();
265        }
266
267        flush0();
268    }
269
270    private void writePad() throws IOException {
271        // write line separator if necessary
272
273        if (lineLength > 0 && linePosition >= lineLength) {
274            writeLineSeparator();
275        }
276
277        // encode data into 4 bytes
278
279        if (encoded.length - position < 4)
280            flush0();
281
282        if (modulus == 1) {
283            encoded[position++] = BASE64_TABLE[(data >> 2) & MASK_6BITS];
284            encoded[position++] = BASE64_TABLE[(data << 4) & MASK_6BITS];
285            encoded[position++] = BASE64_PAD;
286            encoded[position++] = BASE64_PAD;
287        } else {
288            assert modulus == 2;
289            encoded[position++] = BASE64_TABLE[(data >> 10) & MASK_6BITS];
290            encoded[position++] = BASE64_TABLE[(data >> 4) & MASK_6BITS];
291            encoded[position++] = BASE64_TABLE[(data << 2) & MASK_6BITS];
292            encoded[position++] = BASE64_PAD;
293        }
294
295        linePosition += 4;
296    }
297
298    private void writeLineSeparator() throws IOException {
299        linePosition = 0;
300
301        if (encoded.length - position < lineSeparator.length)
302            flush0();
303
304        for (byte ls : lineSeparator)
305            encoded[position++] = ls;
306    }
307
308    private void checkLineSeparator(byte[] lineSeparator) {
309        if (lineSeparator.length > ENCODED_BUFFER_SIZE)
310            throw new IllegalArgumentException("line separator length exceeds "
311                    + ENCODED_BUFFER_SIZE);
312
313        for (byte b : lineSeparator) {
314            if (BASE64_CHARS.contains(b)) {
315                throw new IllegalArgumentException(
316                        "line separator must not contain base64 character '"
317                                + (char) (b & 0xff) + "'");
318            }
319        }
320    }
321}