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.geronimo.mail.util; 21 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.FilterInputStream; 25 26 /** 27 * An implementation of a FilterInputStream that decodes the 28 * stream data in BASE64 encoding format. This version does the 29 * decoding "on the fly" rather than decoding a single block of 30 * data. Since this version is intended for use by the MimeUtilty class, 31 * it also handles line breaks in the encoded data. 32 */ 33 public class Base64DecoderStream extends FilterInputStream { 34 35 static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors"; 36 37 // number of decodeable units we'll try to process at one time. We'll attempt to read that much 38 // data from the input stream and decode in blocks. 39 static protected final int BUFFERED_UNITS = 2000; 40 41 // our decoder for processing the data 42 protected Base64Encoder decoder = new Base64Encoder(); 43 44 // can be overridden by a system property. 45 protected boolean ignoreErrors = false; 46 47 // buffer for reading in chars for decoding (which can support larger bulk reads) 48 protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4]; 49 // a buffer for one decoding unit's worth of data (3 bytes). This is the minimum amount we 50 // can read at one time. 51 protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3]; 52 // count of characters in the buffer 53 protected int decodedCount = 0; 54 // index of the next decoded character 55 protected int decodedIndex = 0; 56 57 58 public Base64DecoderStream(InputStream in) { 59 super(in); 60 // make sure we get the ignore errors flag 61 ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false); 62 } 63 64 /** 65 * Test for the existance of decoded characters in our buffer 66 * of decoded data. 67 * 68 * @return True if we currently have buffered characters. 69 */ 70 private boolean dataAvailable() { 71 return decodedCount != 0; 72 } 73 74 /** 75 * Get the next buffered decoded character. 76 * 77 * @return The next decoded character in the buffer. 78 */ 79 private byte getBufferedChar() { 80 decodedCount--; 81 return decodedChars[decodedIndex++]; 82 } 83 84 /** 85 * Decode a requested number of bytes of data into a buffer. 86 * 87 * @return true if we were able to obtain more data, false otherwise. 88 */ 89 private boolean decodeStreamData() throws IOException { 90 decodedIndex = 0; 91 92 // fill up a data buffer with input data 93 int readCharacters = fillEncodedBuffer(); 94 95 if (readCharacters > 0) { 96 decodedCount = decoder.decode(encodedChars, 0, readCharacters, decodedChars); 97 return true; 98 } 99 return false; 100 } 101 102 103 /** 104 * Retrieve a single byte from the decoded characters buffer. 105 * 106 * @return The decoded character or -1 if there was an EOF condition. 107 */ 108 private int getByte() throws IOException { 109 if (!dataAvailable()) { 110 if (!decodeStreamData()) { 111 return -1; 112 } 113 } 114 decodedCount--; 115 // we need to ensure this doesn't get sign extended 116 return decodedChars[decodedIndex++] & 0xff; 117 } 118 119 private int getBytes(byte[] data, int offset, int length) throws IOException { 120 121 int readCharacters = 0; 122 while (length > 0) { 123 // need data? Try to get some 124 if (!dataAvailable()) { 125 // if we can't get this, return a count of how much we did get (which may be -1). 126 if (!decodeStreamData()) { 127 return readCharacters > 0 ? readCharacters : -1; 128 } 129 } 130 131 // now copy some of the data from the decoded buffer to the target buffer 132 int copyCount = Math.min(decodedCount, length); 133 System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount); 134 decodedIndex += copyCount; 135 decodedCount -= copyCount; 136 offset += copyCount; 137 length -= copyCount; 138 readCharacters += copyCount; 139 } 140 return readCharacters; 141 } 142 143 144 /** 145 * Fill our buffer of input characters for decoding from the 146 * stream. This will attempt read a full buffer, but will 147 * terminate on an EOF or read error. This will filter out 148 * non-Base64 encoding chars and will only return a valid 149 * multiple of 4 number of bytes. 150 * 151 * @return The count of characters read. 152 */ 153 private int fillEncodedBuffer() throws IOException 154 { 155 int readCharacters = 0; 156 157 while (true) { 158 // get the next character from the stream 159 int ch = in.read(); 160 // did we hit an EOF condition? 161 if (ch == -1) { 162 // now check to see if this is normal, or potentially an error 163 // if we didn't get characters as a multiple of 4, we may need to complain about this. 164 if ((readCharacters % 4) != 0) { 165 // the error checking can be turned off...normally it isn't 166 if (!ignoreErrors) { 167 throw new IOException("Base64 encoding error, data truncated"); 168 } 169 // we're ignoring errors, so round down to a multiple and return that. 170 return (readCharacters / 4) * 4; 171 } 172 // return the count. 173 return readCharacters; 174 } 175 // if this character is valid in a Base64 stream, copy it to the buffer. 176 else if (decoder.isValidBase64(ch)) { 177 encodedChars[readCharacters++] = (byte)ch; 178 // if we've filled up the buffer, time to quit. 179 if (readCharacters >= encodedChars.length) { 180 return readCharacters; 181 } 182 } 183 184 // we're filtering out whitespace and CRLF characters, so just ignore these 185 } 186 } 187 188 189 // in order to function as a filter, these streams need to override the different 190 // read() signature. 191 192 public int read() throws IOException 193 { 194 return getByte(); 195 } 196 197 198 public int read(byte [] buffer, int offset, int length) throws IOException { 199 return getBytes(buffer, offset, length); 200 } 201 202 203 public boolean markSupported() { 204 return false; 205 } 206 207 208 public int available() throws IOException { 209 return ((in.available() / 4) * 3) + decodedCount; 210 } 211 }