blob: 6eaa1edcfd8f4a3747862a937002dd748b767f4b [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.io;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.MimeIOException;
import org.apache.james.mime4j.util.ByteArrayBuffer;
import org.apache.james.mime4j.util.CharsetUtil;
import java.io.IOException;
/**
* Stream that constrains itself to a single MIME body part.
* After the stream ends (i.e. read() returns -1) {@link #isLastPart()}
* can be used to determine if a final boundary has been seen or not.
*/
public class MimeBoundaryInputStream extends LineReaderInputStream {
private final byte[] boundary;
private final boolean strict;
private boolean eof;
private int limit;
private boolean atBoundary;
private int boundaryLen;
private boolean lastPart;
private boolean completed;
private final BufferedLineReaderInputStream buffer;
/**
* Store the first buffer length.
* Used to distinguish between an empty preamble and
* no preamble.
*/
private int initialLength;
/**
* Creates a new MimeBoundaryInputStream.
*
* @param inbuffer The underlying stream.
* @param boundary Boundary string (not including leading hyphens).
* @throws IllegalArgumentException when boundary is too long
*/
public MimeBoundaryInputStream(
final BufferedLineReaderInputStream inbuffer,
final String boundary,
final boolean strict) throws IOException {
super(inbuffer);
int bufferSize = 2 * boundary.length();
if (bufferSize < 4096) {
bufferSize = 4096;
}
inbuffer.ensureCapacity(bufferSize);
this.buffer = inbuffer;
this.eof = false;
this.limit = -1;
this.atBoundary = false;
this.boundaryLen = 0;
this.lastPart = false;
this.initialLength = -1;
this.completed = false;
this.strict = strict;
this.boundary = new byte[boundary.length() + 2];
this.boundary[0] = (byte) '-';
this.boundary[1] = (byte) '-';
for (int i = 0; i < boundary.length(); i++) {
byte ch = (byte) boundary.charAt(i);
this.boundary[i + 2] = ch;
}
fillBuffer();
}
/**
* Creates a new MimeBoundaryInputStream.
*
* @param inbuffer The underlying stream.
* @param boundary Boundary string (not including leading hyphens).
* @throws IllegalArgumentException when boundary is too long
*/
public MimeBoundaryInputStream(
final BufferedLineReaderInputStream inbuffer,
final String boundary) throws IOException {
this(inbuffer, boundary, false);
}
/**
* Closes the underlying stream.
*
* @throws IOException on I/O errors.
*/
@Override
public void close() throws IOException {
}
/**
* @see java.io.InputStream#markSupported()
*/
@Override
public boolean markSupported() {
return false;
}
public boolean readAllowed() throws IOException {
if (completed) {
return false;
}
if (endOfStream() && !hasData()) {
skipBoundary();
verifyEndOfStream();
return false;
}
return true;
}
/**
* @see java.io.InputStream#read()
*/
@Override
public int read() throws IOException {
for (;;) {
if (!readAllowed()) return -1;
if (hasData()) {
return buffer.read();
}
fillBuffer();
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
for (;;) {
if (!readAllowed()) return -1;
if (hasData()) {
int chunk = Math.min(len, limit - buffer.pos());
return buffer.read(b, off, chunk);
}
fillBuffer();
}
}
@Override
public int readLine(final ByteArrayBuffer dst) throws IOException {
if (dst == null) {
throw new IllegalArgumentException("Destination buffer may not be null");
}
if (!readAllowed()) return -1;
int total = 0;
boolean found = false;
int bytesRead = 0;
while (!found) {
if (!hasData()) {
bytesRead = fillBuffer();
if (endOfStream() && !hasData()) {
skipBoundary();
verifyEndOfStream();
bytesRead = -1;
break;
}
}
int len = this.limit - this.buffer.pos();
int i = this.buffer.indexOf((byte)'\n', this.buffer.pos(), len);
int chunk;
if (i != -1) {
found = true;
chunk = i + 1 - this.buffer.pos();
} else {
chunk = len;
}
if (chunk > 0) {
dst.append(this.buffer.buf(), this.buffer.pos(), chunk);
this.buffer.skip(chunk);
total += chunk;
}
}
if (total == 0 && bytesRead == -1) {
return -1;
} else {
return total;
}
}
private void verifyEndOfStream() throws IOException {
if (strict && eof && !atBoundary) {
throw new MimeIOException(new MimeException("Unexpected end of stream"));
}
}
private boolean endOfStream() {
return eof || atBoundary;
}
private boolean hasData() {
return limit > buffer.pos() && limit <= buffer.limit();
}
private int fillBuffer() throws IOException {
if (eof) {
return -1;
}
int bytesRead;
if (!hasData()) {
bytesRead = buffer.fillBuffer();
if (bytesRead == -1) {
eof = true;
}
} else {
bytesRead = 0;
}
int i;
int off = buffer.pos();
for (;;) {
i = buffer.indexOf(boundary, off, buffer.limit() - off);
if (i == -1) {
break;
}
// Make sure the boundary is either at the very beginning of the buffer
// or preceded with LF
if (i == buffer.pos() || buffer.byteAt(i - 1) == '\n') {
int pos = i + boundary.length;
int remaining = buffer.limit() - pos;
if (remaining <= 0) {
// Make sure the boundary is terminated with EOS
break;
} else {
// or with a whitespace or '-' char
char ch = (char)(buffer.byteAt(pos));
if (CharsetUtil.isWhitespace(ch) || ch == '-') {
break;
}
}
}
off = i + boundary.length;
}
if (i != -1) {
limit = i;
atBoundary = true;
calculateBoundaryLen();
} else {
if (eof) {
limit = buffer.limit();
} else {
limit = buffer.limit() - (boundary.length + 2);
// [LF] [boundary] [CR][LF] minus one char
}
}
return bytesRead;
}
public boolean isEmptyStream() {
return initialLength == 0;
}
public boolean isFullyConsumed() {
return completed && !buffer.hasBufferedData();
}
private void calculateBoundaryLen() throws IOException {
boundaryLen = boundary.length;
int len = limit - buffer.pos();
if (len >= 0 && initialLength == -1) initialLength = len;
if (len > 0) {
if (buffer.byteAt(limit - 1) == '\n') {
boundaryLen++;
limit--;
}
}
if (len > 1) {
if (buffer.byteAt(limit - 1) == '\r') {
boundaryLen++;
limit--;
}
}
}
private void skipBoundary() throws IOException {
if (!completed) {
completed = true;
buffer.skip(boundaryLen);
boolean checkForLastPart = true;
for (;;) {
if (buffer.length() > 1) {
int ch1 = buffer.byteAt(buffer.pos());
int ch2 = buffer.byteAt(buffer.pos() + 1);
if (checkForLastPart) if (ch1 == '-' && ch2 == '-') {
this.lastPart = true;
buffer.skip(2);
checkForLastPart = false;
continue;
}
if (ch1 == '\r' && ch2 == '\n') {
buffer.skip(2);
break;
} else if (ch1 == '\n') {
buffer.skip(1);
break;
} else {
// ignoring everything in a line starting with a boundary.
buffer.skip(1);
}
} else {
if (eof) {
break;
}
fillBuffer();
}
}
}
}
public boolean isLastPart() {
return lastPart;
}
public boolean eof() {
return eof && !buffer.hasBufferedData();
}
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder("MimeBoundaryInputStream, boundary ");
for (byte b : boundary) {
buffer.append((char) b);
}
return buffer.toString();
}
@Override
public boolean unread(ByteArrayBuffer buf) {
return false;
}
}