View Javadoc

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         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 }