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