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