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 }