001    /**
002     *
003     * Copyright 2003-2006 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package org.apache.geronimo.mail.util;
019    
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.io.FilterInputStream;
023    
024    /**
025     * An implementation of a FilterInputStream that decodes the
026     * stream data in BASE64 encoding format.  This version does the
027     * decoding "on the fly" rather than decoding a single block of
028     * data.  Since this version is intended for use by the MimeUtilty class,
029     * it also handles line breaks in the encoded data.
030     */
031    public class Base64DecoderStream extends FilterInputStream {
032    
033        static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors";
034    
035        // number of decodeable units we'll try to process at one time.  We'll attempt to read that much
036        // data from the input stream and decode in blocks.
037        static protected final int BUFFERED_UNITS = 2000;
038    
039        // our decoder for processing the data
040        protected Base64Encoder decoder = new Base64Encoder();
041    
042        // can be overridden by a system property.
043        protected boolean ignoreErrors = false;
044    
045        // buffer for reading in chars for decoding (which can support larger bulk reads)
046        protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4];
047        // a buffer for one decoding unit's worth of data (3 bytes).  This is the minimum amount we
048        // can read at one time.
049        protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3];
050        // count of characters in the buffer
051        protected int decodedCount = 0;
052        // index of the next decoded character
053        protected int decodedIndex = 0;
054    
055    
056        public Base64DecoderStream(InputStream in) {
057            super(in);
058            // make sure we get the ignore errors flag
059            ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false);
060        }
061    
062        /**
063         * Test for the existance of decoded characters in our buffer
064         * of decoded data.
065         *
066         * @return True if we currently have buffered characters.
067         */
068        private boolean dataAvailable() {
069            return decodedCount != 0;
070        }
071    
072        /**
073         * Get the next buffered decoded character.
074         *
075         * @return The next decoded character in the buffer.
076         */
077        private byte getBufferedChar() {
078            decodedCount--;
079            return decodedChars[decodedIndex++];
080        }
081    
082        /**
083         * Decode a requested number of bytes of data into a buffer.
084         *
085         * @return true if we were able to obtain more data, false otherwise.
086         */
087        private boolean decodeStreamData() throws IOException {
088            decodedIndex = 0;
089    
090            // fill up a data buffer with input data
091            int readCharacters = fillEncodedBuffer();
092    
093            if (readCharacters > 0) {
094                decodedCount =  decoder.decode(encodedChars, 0, readCharacters, decodedChars);
095                return true;
096            }
097            return false;
098        }
099    
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    }