QuotedPrintableInputStream.java revision a07f2ae0b18964aa15e218e8b6be8be24e5c9f46
1/****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one   *
3 * or more contributor license agreements.  See the NOTICE file *
4 * distributed with this work for additional information        *
5 * regarding copyright ownership.  The ASF licenses this file   *
6 * to you under the Apache License, Version 2.0 (the            *
7 * "License"); you may not use this file except in compliance   *
8 * with the License.  You may obtain a copy of the License at   *
9 *                                                              *
10 *   http://www.apache.org/licenses/LICENSE-2.0                 *
11 *                                                              *
12 * Unless required by applicable law or agreed to in writing,   *
13 * software distributed under the License is distributed on an  *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15 * KIND, either express or implied.  See the License for the    *
16 * specific language governing permissions and limitations      *
17 * under the License.                                           *
18 ****************************************************************/
19
20package org.apache.james.mime4j.decoder;
21
22import java.io.IOException;
23import java.io.InputStream;
24
25//BEGIN android-changed: Stubbing out logging
26import org.apache.james.mime4j.Log;
27import org.apache.james.mime4j.LogFactory;
28//END android-changed
29
30/**
31 * Performs Quoted-Printable decoding on an underlying stream.
32 *
33 *
34 *
35 * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
36 */
37public class QuotedPrintableInputStream extends InputStream {
38    private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class);
39
40    private InputStream stream;
41    ByteQueue byteq = new ByteQueue();
42    ByteQueue pushbackq = new ByteQueue();
43    private byte state = 0;
44
45    public QuotedPrintableInputStream(InputStream stream) {
46        this.stream = stream;
47    }
48
49    /**
50     * Closes the underlying stream.
51     *
52     * @throws IOException on I/O errors.
53     */
54    public void close() throws IOException {
55        stream.close();
56    }
57
58    public int read() throws IOException {
59        fillBuffer();
60        if (byteq.count() == 0)
61            return -1;
62        else {
63            byte val = byteq.dequeue();
64            if (val >= 0)
65                return val;
66            else
67                return val & 0xFF;
68        }
69    }
70
71    /**
72     * Pulls bytes out of the underlying stream and places them in the
73     * pushback queue.  This is necessary (vs. reading from the
74     * underlying stream directly) to detect and filter out "transport
75     * padding" whitespace, i.e., all whitespace that appears immediately
76     * before a CRLF.
77     *
78     * @throws IOException Underlying stream threw IOException.
79     */
80    private void populatePushbackQueue() throws IOException {
81        //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!");
82
83        if (pushbackq.count() != 0)
84            return;
85
86        while (true) {
87            int i = stream.read();
88            switch (i) {
89                case -1:
90                    // stream is done
91                    pushbackq.clear();  // discard any whitespace preceding EOF
92                    return;
93                case ' ':
94                case '\t':
95                    pushbackq.enqueue((byte)i);
96                    break;
97                case '\r':
98                case '\n':
99                    pushbackq.clear();  // discard any whitespace preceding EOL
100                    pushbackq.enqueue((byte)i);
101                    return;
102                default:
103                    pushbackq.enqueue((byte)i);
104                    return;
105            }
106        }
107    }
108
109    /**
110     * Causes the pushback queue to get populated if it is empty, then
111     * consumes and decodes bytes out of it until one or more bytes are
112     * in the byte queue.  This decoding step performs the actual QP
113     * decoding.
114     *
115     * @throws IOException Underlying stream threw IOException.
116     */
117    private void fillBuffer() throws IOException {
118        byte msdChar = 0;  // first digit of escaped num
119        while (byteq.count() == 0) {
120            if (pushbackq.count() == 0) {
121                populatePushbackQueue();
122                if (pushbackq.count() == 0)
123                    return;
124            }
125
126            byte b = (byte)pushbackq.dequeue();
127
128            switch (state) {
129                case 0:  // start state, no bytes pending
130                    if (b != '=') {
131                        byteq.enqueue(b);
132                        break;  // state remains 0
133                    } else {
134                        state = 1;
135                        break;
136                    }
137                case 1:  // encountered "=" so far
138                    if (b == '\r') {
139                        state = 2;
140                        break;
141                    } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
142                        state = 3;
143                        msdChar = b;  // save until next digit encountered
144                        break;
145                    } else if (b == '=') {
146                        /*
147                         * Special case when == is encountered.
148                         * Emit one = and stay in this state.
149                         */
150                        if (log.isWarnEnabled()) {
151                            log.warn("Malformed MIME; got ==");
152                        }
153                        byteq.enqueue((byte)'=');
154                        break;
155                    } else {
156                        if (log.isWarnEnabled()) {
157                            log.warn("Malformed MIME; expected \\r or "
158                                    + "[0-9A-Z], got " + b);
159                        }
160                        state = 0;
161                        byteq.enqueue((byte)'=');
162                        byteq.enqueue(b);
163                        break;
164                    }
165                case 2:  // encountered "=\r" so far
166                    if (b == '\n') {
167                        state = 0;
168                        break;
169                    } else {
170                        if (log.isWarnEnabled()) {
171                            log.warn("Malformed MIME; expected "
172                                    + (int)'\n' + ", got " + b);
173                        }
174                        state = 0;
175                        byteq.enqueue((byte)'=');
176                        byteq.enqueue((byte)'\r');
177                        byteq.enqueue(b);
178                        break;
179                    }
180                case 3:  // encountered =<digit> so far; expecting another <digit> to complete the octet
181                    if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
182                        byte msd = asciiCharToNumericValue(msdChar);
183                        byte low = asciiCharToNumericValue(b);
184                        state = 0;
185                        byteq.enqueue((byte)((msd << 4) | low));
186                        break;
187                    } else {
188                        if (log.isWarnEnabled()) {
189                            log.warn("Malformed MIME; expected "
190                                     + "[0-9A-Z], got " + b);
191                        }
192                        state = 0;
193                        byteq.enqueue((byte)'=');
194                        byteq.enqueue(msdChar);
195                        byteq.enqueue(b);
196                        break;
197                    }
198                default:  // should never happen
199                    log.error("Illegal state: " + state);
200                    state = 0;
201                    byteq.enqueue(b);
202                    break;
203            }
204        }
205    }
206
207    /**
208     * Converts '0' => 0, 'A' => 10, etc.
209     * @param c ASCII character value.
210     * @return Numeric value of hexadecimal character.
211     */
212    private byte asciiCharToNumericValue(byte c) {
213        if (c >= '0' && c <= '9') {
214            return (byte)(c - '0');
215        } else if (c >= 'A' && c <= 'Z') {
216            return (byte)(0xA + (c - 'A'));
217        } else if (c >= 'a' && c <= 'z') {
218            return (byte)(0xA + (c - 'a'));
219        } else {
220            /*
221             * This should never happen since all calls to this method
222             * are preceded by a check that c is in [0-9A-Za-z]
223             */
224            throw new IllegalArgumentException((char) c
225                    + " is not a hexadecimal digit");
226        }
227    }
228
229}
230