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    
020    package org.apache.james.mime4j.codec;
021    
022    import java.io.FilterOutputStream;
023    import java.io.IOException;
024    import java.io.OutputStream;
025    import java.util.HashSet;
026    import 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     */
037    public 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    }