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 
20 package org.apache.james.mime4j.decoder;
21 
22 import java.io.IOException;
23 import java.io.InputStream;
24 
25 //BEGIN android-changed: Stubbing out logging
26 import org.apache.james.mime4j.Log;
27 import 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  */
37 public 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 
QuotedPrintableInputStream(InputStream stream)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      */
close()54     public void close() throws IOException {
55         stream.close();
56     }
57 
read()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      */
populatePushbackQueue()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      */
fillBuffer()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      */
asciiCharToNumericValue(byte c)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