View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.mime4j.codec;
21  
22  import java.io.FilterOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStream;
25  import java.util.HashSet;
26  import java.util.Set;
27  
28  /**
29   * This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite>
30   * from RFC 2045 <cite>Multipurpose Internet Mail Extensions (MIME) Part One:
31   * Format of Internet Message Bodies</cite> by Freed and Borenstein.
32   * <p>
33   * Code is based on Base64 and Base64OutputStream code from Commons-Codec 1.4.
34   * 
35   * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
36   */
37  public class Base64OutputStream extends FilterOutputStream {
38  
39      // Default line length per RFC 2045 section 6.8.
40      private static final int DEFAULT_LINE_LENGTH = 76;
41  
42      // CRLF line separator per RFC 2045 section 2.1.
43      private static final byte[] CRLF_SEPARATOR = { '\r', '\n' };
44  
45      // This array is a lookup table that translates 6-bit positive integer index
46      // values into their "Base64 Alphabet" equivalents as specified in Table 1
47      // of RFC 2045.
48      static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F',
49              'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
50              'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
51              'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
52              't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
53              '6', '7', '8', '9', '+', '/' };
54  
55      // Byte used to pad output.
56      private static final byte BASE64_PAD = '=';
57  
58      // This set contains all base64 characters including the pad character. Used
59      // solely to check if a line separator contains any of these characters.
60      private static final Set<Byte> BASE64_CHARS = new HashSet<Byte>();
61  
62      static {
63          for (byte b : BASE64_TABLE) {
64              BASE64_CHARS.add(b);
65          }
66          BASE64_CHARS.add(BASE64_PAD);
67      }
68  
69      // Mask used to extract 6 bits
70      private static final int MASK_6BITS = 0x3f;
71  
72      private static final int ENCODED_BUFFER_SIZE = 2048;
73  
74      private final byte[] singleByte = new byte[1];
75  
76      private final int lineLength;
77      private final byte[] lineSeparator;
78  
79      private boolean closed = false;
80  
81      private final byte[] encoded;
82      private int position = 0;
83  
84      private int data = 0;
85      private int modulus = 0;
86  
87      private int linePosition = 0;
88  
89      /**
90       * Creates a <code>Base64OutputStream</code> that writes the encoded data
91       * to the given output stream using the default line length (76) and line
92       * separator (CRLF).
93       * 
94       * @param out
95       *            underlying output stream.
96       */
97      public Base64OutputStream(OutputStream out) {
98          this(out, DEFAULT_LINE_LENGTH, CRLF_SEPARATOR);
99      }
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 }