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.ByteArrayOutputStream;
021    import java.io.FilterInputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.UnsupportedEncodingException;
025    
026    /**
027     * An implementation of a FilterOutputStream that decodes the
028     * stream data in UU 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 UUDecoderStream extends FilterInputStream {
034        // maximum number of chars that can appear in a single line
035        protected static final int MAX_CHARS_PER_LINE = 45;
036    
037        // our decoder for processing the data
038        protected UUEncoder decoder = new UUEncoder();
039    
040        // a buffer for one decoding unit's worth of data (45 bytes).
041        protected byte[] decodedChars;
042        // count of characters in the buffer
043        protected int decodedCount = 0;
044        // index of the next decoded character
045        protected int decodedIndex = 0;
046    
047        // indicates whether we've already processed the "begin" prefix.
048        protected boolean beginRead = false;
049    
050    
051        public UUDecoderStream(InputStream in) {
052            super(in);
053        }
054    
055    
056        /**
057         * Test for the existance of decoded characters in our buffer
058         * of decoded data.
059         *
060         * @return True if we currently have buffered characters.
061         */
062        private boolean dataAvailable() {
063            return decodedCount != 0;
064        }
065    
066        /**
067         * Get the next buffered decoded character.
068         *
069         * @return The next decoded character in the buffer.
070         */
071        private byte getBufferedChar() {
072            decodedCount--;
073            return decodedChars[decodedIndex++];
074        }
075    
076        /**
077         * Decode a requested number of bytes of data into a buffer.
078         *
079         * @return true if we were able to obtain more data, false otherwise.
080         */
081        private boolean decodeStreamData() throws IOException {
082            decodedIndex = 0;
083    
084            // fill up a data buffer with input data
085            return fillEncodedBuffer() != -1;
086        }
087    
088    
089        /**
090         * Retrieve a single byte from the decoded characters buffer.
091         *
092         * @return The decoded character or -1 if there was an EOF condition.
093         */
094        private int getByte() throws IOException {
095            if (!dataAvailable()) {
096                if (!decodeStreamData()) {
097                    return -1;
098                }
099            }
100            decodedCount--;
101            return decodedChars[decodedIndex++];
102        }
103    
104        private int getBytes(byte[] data, int offset, int length) throws IOException {
105    
106            int readCharacters = 0;
107            while (length > 0) {
108                // need data?  Try to get some
109                if (!dataAvailable()) {
110                    // if we can't get this, return a count of how much we did get (which may be -1).
111                    if (!decodeStreamData()) {
112                        return readCharacters > 0 ? readCharacters : -1;
113                    }
114                }
115    
116                // now copy some of the data from the decoded buffer to the target buffer
117                int copyCount = Math.min(decodedCount, length);
118                System.arraycopy(decodedChars, decodedIndex, data, offset, copyCount);
119                decodedIndex += copyCount;
120                decodedCount -= copyCount;
121                offset += copyCount;
122                length -= copyCount;
123                readCharacters += copyCount;
124            }
125            return readCharacters;
126        }
127    
128        /**
129         * Verify that the first line of the buffer is a valid begin
130         * marker.
131         *
132         * @exception IOException
133         */
134        private void checkBegin() throws IOException {
135            // we only do this the first time we're requested to read from the stream.
136            if (beginRead) {
137                return;
138            }
139    
140            // we might have to skip over lines to reach the marker.  If we hit the EOF without finding
141            // the begin, that's an error.
142            while (true) {
143                String line = readLine();
144                if (line == null) {
145                    throw new IOException("Missing UUEncode begin command");
146                }
147    
148                // is this our begin?
149                if (line.regionMatches(true, 0, "begin ", 0, 6)) {
150                    // This is the droid we're looking for.....
151                    beginRead = true;
152                    return;
153                }
154            }
155        }
156    
157    
158        /**
159         * Read a line of data.  Returns null if there is an EOF.
160         *
161         * @return The next line read from the stream.  Returns null if we
162         *         hit the end of the stream.
163         * @exception IOException
164         */
165        protected String readLine() throws IOException {
166            decodedIndex = 0;
167            // get an accumulator for the data
168            StringBuffer buffer = new StringBuffer();
169    
170            // now process a character at a time.
171            int ch = in.read();
172            while (ch != -1) {
173                // a naked new line completes the line.
174                if (ch == '\n') {
175                    break;
176                }
177                // a carriage return by itself is ignored...we're going to assume that this is followed
178                // by a new line because we really don't have the capability of pushing this back .
179                else if (ch == '\r') {
180                    ;
181                }
182                else {
183                    // add this to our buffer
184                    buffer.append((char)ch);
185                }
186                ch = in.read();
187            }
188    
189            // if we didn't get any data at all, return nothing
190            if (ch == -1 && buffer.length() == 0) {
191                return null;
192            }
193            // convert this into a string.
194            return buffer.toString();
195        }
196    
197    
198        /**
199         * Fill our buffer of input characters for decoding from the
200         * stream.  This will attempt read a full buffer, but will
201         * terminate on an EOF or read error.  This will filter out
202         * non-Base64 encoding chars and will only return a valid
203         * multiple of 4 number of bytes.
204         *
205         * @return The count of characters read.
206         */
207        private int fillEncodedBuffer() throws IOException
208        {
209            checkBegin();
210            // reset the buffer position
211            decodedIndex = 0;
212    
213            while (true) {
214    
215                // we read these as character lines.  We need to be looking for the "end" marker for the
216                // end of the data.
217                String line = readLine();
218    
219                // this should NOT be happening....
220                if (line == null) {
221                    throw new IOException("Missing end in UUEncoded data");
222                }
223    
224                // Is this the end marker?  EOF baby, EOF!
225                if (line.equalsIgnoreCase("end")) {
226                    // this indicates we got nuttin' more to do.
227                    return -1;
228                }
229    
230                ByteArrayOutputStream out = new ByteArrayOutputStream(MAX_CHARS_PER_LINE);
231    
232                byte [] lineBytes;
233                try {
234                    lineBytes = line.getBytes("US-ASCII");
235                } catch (UnsupportedEncodingException e) {
236                    throw new IOException("Invalid UUEncoding");
237                }
238    
239                // decode this line
240                decodedCount = decoder.decode(lineBytes, 0, lineBytes.length, out);
241    
242                // not just a zero-length line?
243                if (decodedCount != 0) {
244                    // get the resulting byte array
245                    decodedChars = out.toByteArray();
246                    return decodedCount;
247                }
248            }
249        }
250    
251    
252        // in order to function as a filter, these streams need to override the different
253        // read() signature.
254    
255        public int read() throws IOException
256        {
257            return getByte();
258        }
259    
260    
261        public int read(byte [] buffer, int offset, int length) throws IOException {
262            return getBytes(buffer, offset, length);
263        }
264    
265    
266        public boolean markSupported() {
267            return false;
268        }
269    
270    
271        public int available() throws IOException {
272            return ((in.available() / 4) * 3) + decodedCount;
273        }
274    }
275