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 }