Home | History | Annotate | Download | only in decoder
      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