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