| /**************************************************************** |
| * Licensed to the Apache Software Foundation (ASF) under one * |
| * or more contributor license agreements. See the NOTICE file * |
| * distributed with this work for additional information * |
| * regarding copyright ownership. The ASF licenses this file * |
| * to you under the Apache License, Version 2.0 (the * |
| * "License"); you may not use this file except in compliance * |
| * with the License. You may obtain a copy of the License at * |
| * * |
| * http://www.apache.org/licenses/LICENSE-2.0 * |
| * * |
| * Unless required by applicable law or agreed to in writing, * |
| * software distributed under the License is distributed on an * |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * |
| * KIND, either express or implied. See the License for the * |
| * specific language governing permissions and limitations * |
| * under the License. * |
| ****************************************************************/ |
| |
| package org.apache.james.mime4j.decoder; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| //BEGIN android-changed: Stubbing out logging |
| import org.apache.james.mime4j.Log; |
| import org.apache.james.mime4j.LogFactory; |
| //END android-changed |
| |
| /** |
| * Performs Quoted-Printable decoding on an underlying stream. |
| * |
| * |
| * |
| * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $ |
| */ |
| public class QuotedPrintableInputStream extends InputStream { |
| private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class); |
| |
| private InputStream stream; |
| ByteQueue byteq = new ByteQueue(); |
| ByteQueue pushbackq = new ByteQueue(); |
| private byte state = 0; |
| |
| public QuotedPrintableInputStream(InputStream stream) { |
| this.stream = stream; |
| } |
| |
| /** |
| * Closes the underlying stream. |
| * |
| * @throws IOException on I/O errors. |
| */ |
| public void close() throws IOException { |
| stream.close(); |
| } |
| |
| public int read() throws IOException { |
| fillBuffer(); |
| if (byteq.count() == 0) |
| return -1; |
| else { |
| byte val = byteq.dequeue(); |
| if (val >= 0) |
| return val; |
| else |
| return val & 0xFF; |
| } |
| } |
| |
| /** |
| * Pulls bytes out of the underlying stream and places them in the |
| * pushback queue. This is necessary (vs. reading from the |
| * underlying stream directly) to detect and filter out "transport |
| * padding" whitespace, i.e., all whitespace that appears immediately |
| * before a CRLF. |
| * |
| * @throws IOException Underlying stream threw IOException. |
| */ |
| private void populatePushbackQueue() throws IOException { |
| //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!"); |
| |
| if (pushbackq.count() != 0) |
| return; |
| |
| while (true) { |
| int i = stream.read(); |
| switch (i) { |
| case -1: |
| // stream is done |
| pushbackq.clear(); // discard any whitespace preceding EOF |
| return; |
| case ' ': |
| case '\t': |
| pushbackq.enqueue((byte)i); |
| break; |
| case '\r': |
| case '\n': |
| pushbackq.clear(); // discard any whitespace preceding EOL |
| pushbackq.enqueue((byte)i); |
| return; |
| default: |
| pushbackq.enqueue((byte)i); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Causes the pushback queue to get populated if it is empty, then |
| * consumes and decodes bytes out of it until one or more bytes are |
| * in the byte queue. This decoding step performs the actual QP |
| * decoding. |
| * |
| * @throws IOException Underlying stream threw IOException. |
| */ |
| private void fillBuffer() throws IOException { |
| byte msdChar = 0; // first digit of escaped num |
| while (byteq.count() == 0) { |
| if (pushbackq.count() == 0) { |
| populatePushbackQueue(); |
| if (pushbackq.count() == 0) |
| return; |
| } |
| |
| byte b = (byte)pushbackq.dequeue(); |
| |
| switch (state) { |
| case 0: // start state, no bytes pending |
| if (b != '=') { |
| byteq.enqueue(b); |
| break; // state remains 0 |
| } else { |
| state = 1; |
| break; |
| } |
| case 1: // encountered "=" so far |
| if (b == '\r') { |
| state = 2; |
| break; |
| } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { |
| state = 3; |
| msdChar = b; // save until next digit encountered |
| break; |
| } else if (b == '=') { |
| /* |
| * Special case when == is encountered. |
| * Emit one = and stay in this state. |
| */ |
| if (log.isWarnEnabled()) { |
| log.warn("Malformed MIME; got =="); |
| } |
| byteq.enqueue((byte)'='); |
| break; |
| } else { |
| if (log.isWarnEnabled()) { |
| log.warn("Malformed MIME; expected \\r or " |
| + "[0-9A-Z], got " + b); |
| } |
| state = 0; |
| byteq.enqueue((byte)'='); |
| byteq.enqueue(b); |
| break; |
| } |
| case 2: // encountered "=\r" so far |
| if (b == '\n') { |
| state = 0; |
| break; |
| } else { |
| if (log.isWarnEnabled()) { |
| log.warn("Malformed MIME; expected " |
| + (int)'\n' + ", got " + b); |
| } |
| state = 0; |
| byteq.enqueue((byte)'='); |
| byteq.enqueue((byte)'\r'); |
| byteq.enqueue(b); |
| break; |
| } |
| case 3: // encountered =<digit> so far; expecting another <digit> to complete the octet |
| if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { |
| byte msd = asciiCharToNumericValue(msdChar); |
| byte low = asciiCharToNumericValue(b); |
| state = 0; |
| byteq.enqueue((byte)((msd << 4) | low)); |
| break; |
| } else { |
| if (log.isWarnEnabled()) { |
| log.warn("Malformed MIME; expected " |
| + "[0-9A-Z], got " + b); |
| } |
| state = 0; |
| byteq.enqueue((byte)'='); |
| byteq.enqueue(msdChar); |
| byteq.enqueue(b); |
| break; |
| } |
| default: // should never happen |
| log.error("Illegal state: " + state); |
| state = 0; |
| byteq.enqueue(b); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Converts '0' => 0, 'A' => 10, etc. |
| * @param c ASCII character value. |
| * @return Numeric value of hexadecimal character. |
| */ |
| private byte asciiCharToNumericValue(byte c) { |
| if (c >= '0' && c <= '9') { |
| return (byte)(c - '0'); |
| } else if (c >= 'A' && c <= 'Z') { |
| return (byte)(0xA + (c - 'A')); |
| } else if (c >= 'a' && c <= 'z') { |
| return (byte)(0xA + (c - 'a')); |
| } else { |
| /* |
| * This should never happen since all calls to this method |
| * are preceded by a check that c is in [0-9A-Za-z] |
| */ |
| throw new IllegalArgumentException((char) c |
| + " is not a hexadecimal digit"); |
| } |
| } |
| |
| } |