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 }