View Javadoc

1   /**
2    *
3    * Copyright 2003-2006 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.geronimo.mail.util;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.FilterInputStream;
23  
24  /**
25   * An implementation of a FilterInputStream that decodes the
26   * stream data in BASE64 encoding format.  This version does the
27   * decoding "on the fly" rather than decoding a single block of
28   * data.  Since this version is intended for use by the MimeUtilty class,
29   * it also handles line breaks in the encoded data.
30   */
31  public class Base64DecoderStream extends FilterInputStream {
32  
33      static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors";
34  
35      // number of decodeable units we'll try to process at one time.  We'll attempt to read that much
36      // data from the input stream and decode in blocks.
37      static protected final int BUFFERED_UNITS = 2000;
38  
39      // our decoder for processing the data
40      protected Base64Encoder decoder = new Base64Encoder();
41  
42      // can be overridden by a system property.
43      protected boolean ignoreErrors = false;
44  
45      // buffer for reading in chars for decoding (which can support larger bulk reads)
46      protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4];
47      // a buffer for one decoding unit's worth of data (3 bytes).  This is the minimum amount we
48      // can read at one time.
49      protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3];
50      // count of characters in the buffer
51      protected int decodedCount = 0;
52      // index of the next decoded character
53      protected int decodedIndex = 0;
54  
55  
56      public Base64DecoderStream(InputStream in) {
57          super(in);
58          // make sure we get the ignore errors flag
59          ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false);
60      }
61  
62      /**
63       * Test for the existance of decoded characters in our buffer
64       * of decoded data.
65       *
66       * @return True if we currently have buffered characters.
67       */
68      private boolean dataAvailable() {
69          return decodedCount != 0;
70      }
71  
72      /**
73       * Get the next buffered decoded character.
74       *
75       * @return The next decoded character in the buffer.
76       */
77      private byte getBufferedChar() {
78          decodedCount--;
79          return decodedChars[decodedIndex++];
80      }
81  
82      /**
83       * Decode a requested number of bytes of data into a buffer.
84       *
85       * @return true if we were able to obtain more data, false otherwise.
86       */
87      private boolean decodeStreamData() throws IOException {
88          decodedIndex = 0;
89  
90          // fill up a data buffer with input data
91          int readCharacters = fillEncodedBuffer();
92  
93          if (readCharacters > 0) {
94              decodedCount =  decoder.decode(encodedChars, 0, readCharacters, decodedChars);
95              return true;
96          }
97          return false;
98      }
99  
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 }