blob: 4d6486708bc11721c06c11b45a6bbbb211064160 [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.codec;
import java.io.IOException;
import java.io.InputStream;
import org.apache.james.mime4j.util.ByteArrayBuffer;
/**
* Performs Quoted-Printable decoding on an underlying stream.
*/
public class QuotedPrintableInputStream extends InputStream {
private static final int DEFAULT_BUFFER_SIZE = 1024 * 2;
private static final byte EQ = 0x3D;
private static final byte CR = 0x0D;
private static final byte LF = 0x0A;
private final byte[] singleByte = new byte[1];
private final InputStream in;
private final ByteArrayBuffer decodedBuf;
private final ByteArrayBuffer blanks;
private final byte[] encoded;
private int pos = 0; // current index into encoded buffer
private int limit = 0; // current size of encoded buffer
private boolean lastWasCR = false;
private boolean closed;
private final DecodeMonitor monitor;
public QuotedPrintableInputStream(final InputStream in, DecodeMonitor monitor) {
this(DEFAULT_BUFFER_SIZE, in, monitor);
}
protected QuotedPrintableInputStream(final int bufsize, final InputStream in, DecodeMonitor monitor) {
super();
this.in = in;
this.encoded = new byte[bufsize];
this.decodedBuf = new ByteArrayBuffer(512);
this.blanks = new ByteArrayBuffer(512);
this.closed = false;
this.monitor = monitor;
}
protected QuotedPrintableInputStream(final int bufsize, final InputStream in, boolean strict) {
this(bufsize, in, strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT);
}
public QuotedPrintableInputStream(final InputStream in, boolean strict) {
this(DEFAULT_BUFFER_SIZE, in, strict);
}
public QuotedPrintableInputStream(final InputStream in) {
this(in, false);
}
/**
* Terminates Quoted-Printable coded content. This method does NOT close
* the underlying input stream.
*
* @throws IOException on I/O errors.
*/
@Override
public void close() throws IOException {
closed = true;
}
private int fillBuffer() throws IOException {
// Compact buffer if needed
if (pos < limit) {
System.arraycopy(encoded, pos, encoded, 0, limit - pos);
limit -= pos;
pos = 0;
} else {
limit = 0;
pos = 0;
}
int capacity = encoded.length - limit;
if (capacity > 0) {
int bytesRead = in.read(encoded, limit, capacity);
if (bytesRead > 0) {
limit += bytesRead;
}
return bytesRead;
} else {
return 0;
}
}
private int getnext() {
if (pos < limit) {
byte b = encoded[pos];
pos++;
return b & 0xFF;
} else {
return -1;
}
}
private int peek(int i) {
if (pos + i < limit) {
return encoded[pos + i] & 0xFF;
} else {
return -1;
}
}
private int transfer(
final int b, final byte[] buffer, final int from, final int to, boolean keepblanks) throws IOException {
int index = from;
if (keepblanks && blanks.length() > 0) {
int chunk = Math.min(blanks.length(), to - index);
System.arraycopy(blanks.buffer(), 0, buffer, index, chunk);
index += chunk;
int remaining = blanks.length() - chunk;
if (remaining > 0) {
decodedBuf.append(blanks.buffer(), chunk, remaining);
}
blanks.clear();
} else if (blanks.length() > 0 && !keepblanks) {
StringBuilder sb = new StringBuilder(blanks.length() * 3);
for (int i = 0; i < blanks.length(); i++) sb.append(" "+blanks.byteAt(i));
if (monitor.warn("ignored blanks", sb.toString()))
throw new IOException("ignored blanks");
}
if (b != -1) {
if (index < to) {
buffer[index++] = (byte) b;
} else {
decodedBuf.append(b);
}
}
return index;
}
private int read0(final byte[] buffer, final int off, final int len) throws IOException {
boolean eof = false;
int from = off;
int to = off + len;
int index = off;
// check if a previous invocation left decoded content
if (decodedBuf.length() > 0) {
int chunk = Math.min(decodedBuf.length(), to - index);
System.arraycopy(decodedBuf.buffer(), 0, buffer, index, chunk);
decodedBuf.remove(0, chunk);
index += chunk;
}
while (index < to) {
if (limit - pos < 3) {
int bytesRead = fillBuffer();
eof = bytesRead == -1;
}
// end of stream?
if (limit - pos == 0 && eof) {
return index == from ? -1 : index - from;
}
while (pos < limit && index < to) {
int b = encoded[pos++] & 0xFF;
if (lastWasCR && b != LF) {
if (monitor.warn("Found CR without LF", "Leaving it as is")) {
throw new IOException("Found CR without LF");
}
index = transfer(CR, buffer, index, to, false);
} else if (!lastWasCR && b == LF) {
if (monitor.warn("Found LF without CR", "Translating to CRLF")) {
throw new IOException("Found LF without CR");
}
}
if (b == CR) {
lastWasCR = true;
continue;
} else {
lastWasCR = false;
}
if (b == LF) {
// at end of line
if (blanks.length() == 0) {
index = transfer(CR, buffer, index, to, false);
index = transfer(LF, buffer, index, to, false);
} else {
if (blanks.byteAt(0) != EQ) {
// hard line break
index = transfer(CR, buffer, index, to, false);
index = transfer(LF, buffer, index, to, false);
}
}
blanks.clear();
} else if (b == EQ) {
if (limit - pos < 2 && !eof) {
// not enough buffered data
pos--;
break;
}
// found special char '='
int b2 = getnext();
if (b2 == EQ) {
index = transfer(b2, buffer, index, to, true);
// deal with '==\r\n' brokenness
int bb1 = peek(0);
int bb2 = peek(1);
if (bb1 == LF || (bb1 == CR && bb2 == LF)) {
monitor.warn("Unexpected ==EOL encountered", "== 0x"+bb1+" 0x"+bb2);
blanks.append(b2);
} else {
monitor.warn("Unexpected == encountered", "==");
}
} else if (Character.isWhitespace((char) b2)) {
// soft line break
int b3 = peek(0);
if (!(b2 == CR && b3 == LF)) {
if (monitor.warn("Found non-standard soft line break", "Translating to soft line break")) {
throw new IOException("Non-standard soft line break");
}
}
if (b3 == LF) {
lastWasCR = b2 == CR;
}
index = transfer(-1, buffer, index, to, true);
if (b2 != LF) {
blanks.append(b);
blanks.append(b2);
}
} else {
int b3 = getnext();
int upper = convert(b2);
int lower = convert(b3);
if (upper < 0 || lower < 0) {
monitor.warn("Malformed encoded value encountered", "leaving "+((char) EQ)+((char) b2)+((char) b3)+" as is");
// TODO see MIME4J-160
index = transfer(EQ, buffer, index, to, true);
index = transfer(b2, buffer, index, to, false);
index = transfer(b3, buffer, index, to, false);
} else {
index = transfer((upper << 4) | lower, buffer, index, to, true);
}
}
} else if (Character.isWhitespace(b)) {
blanks.append(b);
} else {
index = transfer((int) b & 0xFF, buffer, index, to, true);
}
}
}
return to - from;
}
/**
* Converts '0' => 0, 'A' => 10, etc.
* @param c ASCII character value.
* @return Numeric value of hexadecimal character.
*/
private int convert(int c) {
if (c >= '0' && c <= '9') {
return (c - '0');
} else if (c >= 'A' && c <= 'F') {
return (0xA + (c - 'A'));
} else if (c >= 'a' && c <= 'f') {
return (0xA + (c - 'a'));
} else {
return -1;
}
}
@Override
public int read() throws IOException {
if (closed) {
throw new IOException("Stream has been closed");
}
for (;;) {
int bytes = read(singleByte, 0, 1);
if (bytes == -1) {
return -1;
}
if (bytes == 1) {
return singleByte[0] & 0xff;
}
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("Stream has been closed");
}
return read0(b, off, len);
}
}