blob: 1ad711ff1b91a04c1115574b2c9b37fd7424be5c [file] [log] [blame]
/****************************************************************
* 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;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 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 = 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");
}
}
}