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}