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    public class Base64DecoderStream extends FilterInputStream {
034    
035        static protected final String MAIL_BASE64_IGNOREERRORS = "mail.mime.base64.ignoreerrors";
036    
037        // number of decodeable units we'll try to process at one time.  We'll attempt to read that much
038        // data from the input stream and decode in blocks.
039        static protected final int BUFFERED_UNITS = 2000;
040    
041        // our decoder for processing the data
042        protected Base64Encoder decoder = new Base64Encoder();
043    
044        // can be overridden by a system property.
045        protected boolean ignoreErrors = false;
046    
047        // buffer for reading in chars for decoding (which can support larger bulk reads)
048        protected byte[] encodedChars = new byte[BUFFERED_UNITS * 4];
049        // a buffer for one decoding unit's worth of data (3 bytes).  This is the minimum amount we
050        // can read at one time.
051        protected byte[] decodedChars = new byte[BUFFERED_UNITS * 3];
052        // count of characters in the buffer
053        protected int decodedCount = 0;
054        // index of the next decoded character
055        protected int decodedIndex = 0;
056    
057    
058        public Base64DecoderStream(InputStream in) {
059            super(in);
060            // make sure we get the ignore errors flag
061            ignoreErrors = SessionUtil.getBooleanProperty(MAIL_BASE64_IGNOREERRORS, false);
062        }
063    
064        /**
065         * Test for the existance of decoded characters in our buffer
066         * of decoded data.
067         *
068         * @return True if we currently have buffered characters.
069         */
070        private boolean dataAvailable() {
071            return decodedCount != 0;
072        }
073    
074        /**
075         * Get the next buffered decoded character.
076         *
077         * @return The next decoded character in the buffer.
078         */
079        private byte getBufferedChar() {
080            decodedCount--;
081            return decodedChars[decodedIndex++];
082        }
083    
084        /**
085         * Decode a requested number of bytes of data into a buffer.
086         *
087         * @return true if we were able to obtain more data, false otherwise.
088         */
089        private boolean decodeStreamData() throws IOException {
090            decodedIndex = 0;
091    
092            // fill up a data buffer with input data
093            int readCharacters = fillEncodedBuffer();
094    
095            if (readCharacters > 0) {
096                decodedCount =  decoder.decode(encodedChars, 0, readCharacters, decodedChars);
097                return true;
098            }
099            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    }