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 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