001 /** 002 * 003 * Copyright 2003-2006 The Apache Software Foundation 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.mail.util; 019 020 import java.io.IOException; 021 import java.io.InputStream; 022 import java.io.FilterInputStream; 023 024 /** 025 * An implementation of a FilterInputStream that decodes the 026 * stream data in BASE64 encoding format. This version does the 027 * decoding "on the fly" rather than decoding a single block of 028 * data. Since this version is intended for use by the MimeUtilty class, 029 * it also handles line breaks in the encoded data. 030 */ 031 public class Base64DecoderStream extends FilterInputStream { 032 033 static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors"; 034 035 // number of decodeable units we'll try to process at one time. We'll attempt to read that much 036 // data from the input stream and decode in blocks. 037 static protected final int BUFFERED_UNITS = 2000; 038 039 // our decoder for processing the data 040 protected Base64Encoder decoder = new Base64Encoder(); 041 042 // can be overridden by a system property. 043 protected boolean ignoreErrors = false; 044 045 // buffer for reading in chars for decoding (which can support larger bulk reads) 046 protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4]; 047 // a buffer for one decoding unit's worth of data (3 bytes). This is the minimum amount we 048 // can read at one time. 049 protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3]; 050 // count of characters in the buffer 051 protected int decodedCount = 0; 052 // index of the next decoded character 053 protected int decodedIndex = 0; 054 055 056 public Base64DecoderStream(InputStream in) { 057 super(in); 058 // make sure we get the ignore errors flag 059 ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false); 060 } 061 062 /** 063 * Test for the existance of decoded characters in our buffer 064 * of decoded data. 065 * 066 * @return True if we currently have buffered characters. 067 */ 068 private boolean dataAvailable() { 069 return decodedCount != 0; 070 } 071 072 /** 073 * Get the next buffered decoded character. 074 * 075 * @return The next decoded character in the buffer. 076 */ 077 private byte getBufferedChar() { 078 decodedCount--; 079 return decodedChars[decodedIndex++]; 080 } 081 082 /** 083 * Decode a requested number of bytes of data into a buffer. 084 * 085 * @return true if we were able to obtain more data, false otherwise. 086 */ 087 private boolean decodeStreamData() throws IOException { 088 decodedIndex = 0; 089 090 // fill up a data buffer with input data 091 int readCharacters = fillEncodedBuffer(); 092 093 if (readCharacters > 0) { 094 decodedCount = decoder.decode(encodedChars, 0, readCharacters, decodedChars); 095 return true; 096 } 097 return false; 098 } 099 100 101 /** 102 * Retrieve a single byte from the decoded characters buffer. 103 * 104 * @return The decoded character or -1 if there was an EOF condition. 105 */ 106 private int getByte() throws IOException { 107 if (!dataAvailable()) { 108 if (!decodeStreamData()) { 109 return -1; 110 } 111 } 112 decodedCount--; 113 return decodedChars[decodedIndex++]; 114 } 115 116 private int getBytes(byte[] data, int offset, int length) throws IOException { 117 118 int readCharacters = 0; 119 while (length > 0) { 120 // need data? Try to get some 121 if (!dataAvailable()) { 122 // if we can't get this, return a count of how much we did get (which may be -1). 123 if (!decodeStreamData()) { 124 return readCharacters > 0 ? readCharacters : -1; 125 } 126 } 127 128 // now copy some of the data from the decoded buffer to the target buffer 129 int copyCount = Math.min(decodedCount, length); 130 System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount); 131 decodedIndex += copyCount; 132 decodedCount -= copyCount; 133 offset += copyCount; 134 length -= copyCount; 135 readCharacters += copyCount; 136 } 137 return readCharacters; 138 } 139 140 141 /** 142 * Fill our buffer of input characters for decoding from the 143 * stream. This will attempt read a full buffer, but will 144 * terminate on an EOF or read error. This will filter out 145 * non-Base64 encoding chars and will only return a valid 146 * multiple of 4 number of bytes. 147 * 148 * @return The count of characters read. 149 */ 150 private int fillEncodedBuffer() throws IOException 151 { 152 int readCharacters = 0; 153 154 while (true) { 155 // get the next character from the stream 156 int ch = in.read(); 157 // did we hit an EOF condition? 158 if (ch == -1) { 159 // now check to see if this is normal, or potentially an error 160 // if we didn't get characters as a multiple of 4, we may need to complain about this. 161 if ((readCharacters % 4) != 0) { 162 // the error checking can be turned off...normally it isn't 163 if (!ignoreErrors) { 164 throw new IOException("Base64 encoding error, data truncated"); 165 } 166 // we're ignoring errors, so round down to a multiple and return that. 167 return (readCharacters / 4) * 4; 168 } 169 // return the count. 170 return readCharacters; 171 } 172 // if this character is valid in a Base64 stream, copy it to the buffer. 173 else if (decoder.isValidBase64(ch)) { 174 encodedChars[readCharacters++] = (byte)ch; 175 // if we've filled up the buffer, time to quit. 176 if (readCharacters >= encodedChars.length) { 177 return readCharacters; 178 } 179 } 180 181 // we're filtering out whitespace and CRLF characters, so just ignore these 182 } 183 } 184 185 186 // in order to function as a filter, these streams need to override the different 187 // read() signature. 188 189 public int read() throws IOException 190 { 191 return getByte(); 192 } 193 194 195 public int read(byte [] buffer, int offset, int length) throws IOException { 196 return getBytes(buffer, offset, length); 197 } 198 199 200 public boolean markSupported() { 201 return false; 202 } 203 204 205 public int available() throws IOException { 206 return ((in.available() / 4) * 3) + decodedCount; 207 } 208 }