From d438fb0d1d9f2c25e85114c1ecc73263c1cc21d5 Mon Sep 17 00:00:00 2001 From: Robert Burrell Donkin Date: Tue, 8 Jul 2008 20:44:06 +0000 Subject: [PATCH] MIME4J-5 Performance patch 3, https://issues.apache.org/jira/browse/MIME4J-5. Contributed by Oleg Kalnichevski. This patch eliminates one-byte-reads for common use cases; eliminates the synchronised StringBuffer and reduces memory footprint. git-svn-id: https://svn.eu.apache.org/repos/asf/james/mime4j/trunk@674944 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/james/mime4j/AbstractEntity.java | 158 ++++----- ...tStream.java => BasicBufferingInputStream.java} | 48 ++- .../apache/james/mime4j/BufferingInputStream.java | 94 +++--- ...tream.java => BufferingInputStreamAdaptor.java} | 164 ++++++---- .../org/apache/james/mime4j/ByteArrayBuffer.java | 141 ++++++++ .../org/apache/james/mime4j/CharArrayBuffer.java | 247 ++++++++++++++ src/main/java/org/apache/james/mime4j/Event.java | 3 + .../java/org/apache/james/mime4j/InputBuffer.java | 54 +++- .../james/mime4j/MimeBoundaryInputStream.java | 88 ++++- .../java/org/apache/james/mime4j/MimeEntity.java | 29 +- .../org/apache/james/mime4j/MimeTokenStream.java | 4 +- .../org/apache/james/mime4j/util/MessageUtils.java | 10 + .../mime4j/BasicBufferingInputStreamTest.java | 144 +++++++++ .../mime4j/BufferingInputStreamAdaptorTest.java | 144 +++++++++ .../org/apache/james/mime4j/InputBufferTest.java | 129 ++++++++ .../james/mime4j/MimeBoundaryInputStreamTest.java | 116 ++++++- .../org/apache/james/mime4j/MimeEntityTest.java | 6 +- .../james/mime4j/StrictMimeTokenStreamTest.java | 1 + .../apache/james/mime4j/TestByteArrayBuffer.java | 229 +++++++++++++ .../apache/james/mime4j/TestCharArrayBuffer.java | 357 +++++++++++++++++++++ 20 files changed, 1927 insertions(+), 239 deletions(-) copy src/main/java/org/apache/james/mime4j/{BufferingInputStream.java => BasicBufferingInputStream.java} (56%) rename src/main/java/org/apache/james/mime4j/{EOFSensitiveInputStream.java => BufferingInputStreamAdaptor.java} (62%) create mode 100644 src/main/java/org/apache/james/mime4j/ByteArrayBuffer.java create mode 100644 src/main/java/org/apache/james/mime4j/CharArrayBuffer.java create mode 100644 src/test/java/org/apache/james/mime4j/BasicBufferingInputStreamTest.java create mode 100644 src/test/java/org/apache/james/mime4j/BufferingInputStreamAdaptorTest.java create mode 100644 src/test/java/org/apache/james/mime4j/TestByteArrayBuffer.java create mode 100644 src/test/java/org/apache/james/mime4j/TestCharArrayBuffer.java diff --git a/src/main/java/org/apache/james/mime4j/AbstractEntity.java b/src/main/java/org/apache/james/mime4j/AbstractEntity.java index 5a2efc0..1eaa2ba 100644 --- a/src/main/java/org/apache/james/mime4j/AbstractEntity.java +++ b/src/main/java/org/apache/james/mime4j/AbstractEntity.java @@ -20,11 +20,11 @@ package org.apache.james.mime4j; import java.io.IOException; -import java.io.InputStream; import java.util.BitSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.MessageUtils; /** * Abstract MIME entity. @@ -33,7 +33,6 @@ public abstract class AbstractEntity implements EntityStateMachine { protected final Log log; - protected final RootInputStream rootStream; protected final BodyDescriptor parent; protected final int startState; protected final int endState; @@ -43,12 +42,12 @@ public abstract class AbstractEntity implements EntityStateMachine { protected int state; - private final StringBuffer sb = new StringBuffer(); - - private int pos, start; - private int lineNumber, startLineNumber; - + private final ByteArrayBuffer linebuf; + private final CharArrayBuffer fieldbuf; + + private int lineCount; private String field, fieldName, fieldValue; + private boolean endOfHeader; private static final BitSet fieldChars = new BitSet(); @@ -71,14 +70,12 @@ public abstract class AbstractEntity implements EntityStateMachine { private static final int T_IN_MESSAGE = -3; AbstractEntity( - RootInputStream rootStream, BodyDescriptor parent, int startState, int endState, boolean maximalBodyDescriptor, boolean strictParsing) { this.log = LogFactory.getLog(getClass()); - this.rootStream = rootStream; this.parent = parent; this.state = startState; this.startState = startState; @@ -86,6 +83,10 @@ public abstract class AbstractEntity implements EntityStateMachine { this.maximalBodyDescriptor = maximalBodyDescriptor; this.strictParsing = strictParsing; this.body = newBodyDescriptor(parent); + this.linebuf = new ByteArrayBuffer(64); + this.fieldbuf = new CharArrayBuffer(64); + this.lineCount = 0; + this.endOfHeader = false; } public int getState() { @@ -106,83 +107,92 @@ public abstract class AbstractEntity implements EntityStateMachine { } return result; } + + protected abstract int getLineNumber(); - protected abstract InputStream getDataStream(); + protected abstract BufferingInputStream getDataStream(); - protected void initHeaderParsing() throws IOException, MimeException { - startLineNumber = lineNumber = rootStream.getLineNumber(); - - InputStream instream = getDataStream(); - - int curr = 0; - int prev = 0; - while ((curr = instream.read()) != -1) { - if (curr == '\n' && (prev == '\n' || prev == 0)) { - /* - * [\r]\n[\r]\n or an immediate \r\n have been seen. - */ - sb.deleteCharAt(sb.length() - 1); + private void fillFieldBuffer() throws IOException, MimeException { + if (endOfHeader) { + return; + } + BufferingInputStream instream = getDataStream(); + fieldbuf.clear(); + for (;;) { + // If there's still data stuck in the line buffer + // copy it to the field buffer + int len = linebuf.length(); + if (len > 0) { + fieldbuf.append(linebuf, 0, len); + } + linebuf.clear(); + if (instream.readLine(linebuf) == -1) { + monitor(Event.HEADERS_PREMATURE_END); + endOfHeader = true; break; } - sb.append((char) curr); - prev = curr == '\r' ? prev : curr; - } - - if (curr == -1) { - monitor(Event.HEADERS_PREMATURE_END); + len = linebuf.length(); + if (len > 0 && linebuf.byteAt(len - 1) == '\n') { + len--; + } + if (len > 0 && linebuf.byteAt(len - 1) == '\r') { + len--; + } + if (len == 0) { + // empty line detected + endOfHeader = true; + break; + } + lineCount++; + if (lineCount > 1) { + int ch = linebuf.byteAt(0); + if (ch != MessageUtils.SP && ch != MessageUtils.HT) { + // new header detected + break; + } + } } } - protected boolean parseField() { - while (pos < sb.length()) { - while (pos < sb.length() && sb.charAt(pos) != '\r') { - pos++; + protected boolean parseField() throws IOException { + for (;;) { + if (endOfHeader) { + return false; } - if (pos < sb.length() - 1 && sb.charAt(pos + 1) != '\n') { - pos++; - continue; + fillFieldBuffer(); + + // Strip away line delimiter + int len = fieldbuf.length(); + if (len > 0 && fieldbuf.charAt(len - 1) == '\n') { + len--; } - if (pos >= sb.length() - 2 || fieldChars.get(sb.charAt(pos + 2))) { - /* - * field should be the complete field data excluding the - * trailing \r\n. - */ - field = sb.substring(start, pos); - start = pos + 2; - - /* - * Check for a valid field. - */ - int index = field.indexOf(':'); - boolean valid = false; - if (index != -1 && fieldChars.get(field.charAt(0))) { - valid = true; - fieldName = field.substring(0, index).trim(); - for (int i = 0; i < fieldName.length(); i++) { - if (!fieldChars.get(fieldName.charAt(i))) { - valid = false; - break; - } - } - if (valid) { - fieldValue = field.substring(index + 1); - body.addField(fieldName, fieldValue); - startLineNumber = lineNumber; - pos += 2; - lineNumber++; - return true; + if (len > 0 && fieldbuf.charAt(len - 1) == '\r') { + len--; + } + fieldbuf.setLength(len); + + boolean valid = true; + field = fieldbuf.toString(); + int pos = fieldbuf.indexOf(':'); + if (pos == -1) { + monitor(Event.INALID_HEADER); + valid = false; + } else { + fieldName = fieldbuf.substring(0, pos); + for (int i = 0; i < fieldName.length(); i++) { + if (!fieldChars.get(fieldName.charAt(i))) { + monitor(Event.INALID_HEADER); + valid = false; + break; } } - if (log.isWarnEnabled()) { - log.warn("Line " + startLineNumber - + ": Ignoring invalid field: '" + field.trim() + "'"); - } - startLineNumber = lineNumber; + fieldValue = fieldbuf.substring(pos + 1, fieldbuf.length()); + } + if (valid) { + body.addField(fieldName, fieldValue); + return true; } - pos += 2; - lineNumber++; } - return false; } /** @@ -278,7 +288,7 @@ public abstract class AbstractEntity implements EntityStateMachine { * or for logging */ protected String message(Event event) { - String preamble = "Line " + rootStream.getLineNumber() + ": "; + String preamble = "Line " + getLineNumber() + ": "; final String message; if (event == null) { message = "Event is unexpectedly null."; diff --git a/src/main/java/org/apache/james/mime4j/BufferingInputStream.java b/src/main/java/org/apache/james/mime4j/BasicBufferingInputStream.java similarity index 56% copy from src/main/java/org/apache/james/mime4j/BufferingInputStream.java copy to src/main/java/org/apache/james/mime4j/BasicBufferingInputStream.java index 62034b4..50bc06e 100644 --- a/src/main/java/org/apache/james/mime4j/BufferingInputStream.java +++ b/src/main/java/org/apache/james/mime4j/BasicBufferingInputStream.java @@ -20,20 +20,21 @@ package org.apache.james.mime4j; import java.io.IOException; -import java.io.InputStream; /** - * Implementation of {@link InputStream} backed by an {@link InputBuffer} instance. + * Implementation of {@link BufferingInputStream} backed by an {@link InputBuffer} instance. + * + * @version $Id$ */ -public class BufferingInputStream extends InputStream { +public class BasicBufferingInputStream extends BufferingInputStream { private final InputBuffer buffer; - - public BufferingInputStream(final InputBuffer buffer) { + + public BasicBufferingInputStream(final InputBuffer buffer) { super(); this.buffer = buffer; } - + public void close() throws IOException { this.buffer.closeStream(); } @@ -50,4 +51,39 @@ public class BufferingInputStream extends InputStream { return this.buffer.read(b, off, len); } + public int readLine(final ByteArrayBuffer linebuf) throws IOException { + if (linebuf == null) { + throw new IllegalArgumentException("Buffer may not be null"); + } + int total = 0; + boolean found = false; + int bytesRead = 0; + while (!found) { + if (!this.buffer.hasBufferedData()) { + bytesRead = this.buffer.fillBuffer(); + if (bytesRead == -1) { + break; + } + } + int i = this.buffer.indexOf((byte)'\n'); + int chunk; + if (i != -1) { + found = true; + chunk = i + 1 - this.buffer.pos(); + } else { + chunk = this.buffer.length(); + } + if (chunk > 0) { + linebuf.append(this.buffer.buf(), this.buffer.pos(), chunk); + this.buffer.skip(chunk); + total += chunk; + } + } + if (total == 0 && bytesRead == -1) { + return -1; + } else { + return total; + } + } + } diff --git a/src/main/java/org/apache/james/mime4j/BufferingInputStream.java b/src/main/java/org/apache/james/mime4j/BufferingInputStream.java index 62034b4..a33f17f 100644 --- a/src/main/java/org/apache/james/mime4j/BufferingInputStream.java +++ b/src/main/java/org/apache/james/mime4j/BufferingInputStream.java @@ -1,53 +1,41 @@ -/**************************************************************** - * 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; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Implementation of {@link InputStream} backed by an {@link InputBuffer} instance. - */ -public class BufferingInputStream extends InputStream { - - private final InputBuffer buffer; - - public BufferingInputStream(final InputBuffer buffer) { - super(); - this.buffer = buffer; - } - - public void close() throws IOException { - this.buffer.closeStream(); - } - - public boolean markSupported() { - return false; - } - - public int read() throws IOException { - return this.buffer.read(); - } - - public int read(byte[] b, int off, int len) throws IOException { - return this.buffer.read(b, off, len); - } - -} +/**************************************************************** + * 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; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Input stream capable of reading lines of text. + */ +public abstract class BufferingInputStream extends InputStream { + + /** + * Reads one line of text into the given {@link ByteArrayBuffer}. + * + * @param dst Destination + * @return number of bytes copied or -1 if the end of + * the stream has been reached. + * + * @throws IOException in case of an I/O error. + */ + public abstract int readLine(final ByteArrayBuffer dst) throws IOException; + +} diff --git a/src/main/java/org/apache/james/mime4j/EOFSensitiveInputStream.java b/src/main/java/org/apache/james/mime4j/BufferingInputStreamAdaptor.java similarity index 62% rename from src/main/java/org/apache/james/mime4j/EOFSensitiveInputStream.java rename to src/main/java/org/apache/james/mime4j/BufferingInputStreamAdaptor.java index 565cf96..d1c9751 100644 --- a/src/main/java/org/apache/james/mime4j/EOFSensitiveInputStream.java +++ b/src/main/java/org/apache/james/mime4j/BufferingInputStreamAdaptor.java @@ -1,64 +1,100 @@ -/**************************************************************** - * 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; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * InputStream used by the MIME parser to detect whether the - * underlying data stream was used (read from) and whether the end of the - * stream was reached. - * - * @version $Id$ - */ -class EOFSensitiveInputStream extends FilterInputStream { - - private boolean used = false; - private boolean eof = false; - - public EOFSensitiveInputStream(InputStream is) { - super(is); - } - - public int read() throws IOException { - int i = super.read(); - this.eof = i == -1; - this.used = true; - return i; - } - - public int read(byte[] b, int off, int len) throws IOException { - int i = super.read(b, off, len); - this.eof = i == -1; - this.used = true; - return i; - } - - public boolean eof() { - return this.eof; - } - - public boolean isUsed() { - return this.used; - } - -} +/**************************************************************** + * 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; + +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStream used by the MIME parser to detect whether the + * underlying data stream was used (read from) and whether the end of the + * stream was reached. + * + * @version $Id$ + */ +class BufferingInputStreamAdaptor extends BufferingInputStream { + + private final InputStream is; + private final BufferingInputStream bis; + private boolean used = false; + private boolean eof = false; + + public BufferingInputStreamAdaptor(final InputStream is) { + super(); + this.is = is; + if (is instanceof BufferingInputStream) { + this.bis = (BufferingInputStream) is; + } else { + this.bis = null; + } + } + + public int read() throws IOException { + int i = this.is.read(); + this.eof = i == -1; + this.used = true; + return i; + } + + public int read(byte[] b, int off, int len) throws IOException { + int i = this.is.read(b, off, len); + this.eof = i == -1; + this.used = true; + return i; + } + + public int readLine(final ByteArrayBuffer dst) throws IOException { + int i; + if (this.bis != null) { + i = this.bis.readLine(dst); + } else { + i = doReadLine(dst); + } + this.eof = i == -1; + this.used = true; + return i; + } + + private int doReadLine(final ByteArrayBuffer dst) throws IOException { + int total = 0; + int ch; + while ((ch = this.is.read()) != -1) { + dst.append(ch); + total++; + if (ch == '\n') { + break; + } + } + if (total == 0 && ch == -1) { + return -1; + } else { + return total; + } + } + + public boolean eof() { + return this.eof; + } + + public boolean isUsed() { + return this.used; + } + +} diff --git a/src/main/java/org/apache/james/mime4j/ByteArrayBuffer.java b/src/main/java/org/apache/james/mime4j/ByteArrayBuffer.java new file mode 100644 index 0000000..7617b58 --- /dev/null +++ b/src/main/java/org/apache/james/mime4j/ByteArrayBuffer.java @@ -0,0 +1,141 @@ +/**************************************************************** + * 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; + +/** + * A resizable byte array. + */ +public final class ByteArrayBuffer { + + private byte[] buffer; + private int len; + + public ByteArrayBuffer(int capacity) { + super(); + if (capacity < 0) { + throw new IllegalArgumentException("Buffer capacity may not be negative"); + } + this.buffer = new byte[capacity]; + } + + private void expand(int newlen) { + byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)]; + System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); + this.buffer = newbuffer; + } + + public void append(final byte[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int newlen = this.len + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + System.arraycopy(b, off, this.buffer, this.len, len); + this.len = newlen; + } + + public void append(int b) { + int newlen = this.len + 1; + if (newlen > this.buffer.length) { + expand(newlen); + } + this.buffer[this.len] = (byte)b; + this.len = newlen; + } + + public void append(final char[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int oldlen = this.len; + int newlen = oldlen + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { + this.buffer[i2] = (byte) b[i1]; + } + this.len = newlen; + } + + public void clear() { + this.len = 0; + } + + public byte[] toByteArray() { + byte[] b = new byte[this.len]; + if (this.len > 0) { + System.arraycopy(this.buffer, 0, b, 0, this.len); + } + return b; + } + + public int byteAt(int i) { + return this.buffer[i]; + } + + public int capacity() { + return this.buffer.length; + } + + public int length() { + return this.len; + } + + public byte[] buffer() { + return this.buffer; + } + + public void setLength(int len) { + if (len < 0 || len > this.buffer.length) { + throw new IndexOutOfBoundsException(); + } + this.len = len; + } + + public boolean isEmpty() { + return this.len == 0; + } + + public boolean isFull() { + return this.len == this.buffer.length; + } + + public String toString() { + return new String(toByteArray()); + } + +} diff --git a/src/main/java/org/apache/james/mime4j/CharArrayBuffer.java b/src/main/java/org/apache/james/mime4j/CharArrayBuffer.java new file mode 100644 index 0000000..fd9453f --- /dev/null +++ b/src/main/java/org/apache/james/mime4j/CharArrayBuffer.java @@ -0,0 +1,247 @@ +/**************************************************************** + * 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; + +import org.apache.james.mime4j.util.MessageUtils; + +/** + * A resizable char array. + * + */ +public final class CharArrayBuffer { + + private char[] buffer; + private int len; + + public CharArrayBuffer(int capacity) { + super(); + if (capacity < 0) { + throw new IllegalArgumentException("Buffer capacity may not be negative"); + } + this.buffer = new char[capacity]; + } + + private void expand(int newlen) { + char newbuffer[] = new char[Math.max(this.buffer.length << 1, newlen)]; + System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); + this.buffer = newbuffer; + } + + public void append(final char[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int newlen = this.len + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + System.arraycopy(b, off, this.buffer, this.len, len); + this.len = newlen; + } + + public void append(String str) { + if (str == null) { + str = "null"; + } + int strlen = str.length(); + int newlen = this.len + strlen; + if (newlen > this.buffer.length) { + expand(newlen); + } + str.getChars(0, strlen, this.buffer, this.len); + this.len = newlen; + } + + public void append(final CharArrayBuffer b, int off, int len) { + if (b == null) { + return; + } + append(b.buffer, off, len); + } + + public void append(final CharArrayBuffer b) { + if (b == null) { + return; + } + append(b.buffer,0, b.len); + } + + public void append(char ch) { + int newlen = this.len + 1; + if (newlen > this.buffer.length) { + expand(newlen); + } + this.buffer[this.len] = ch; + this.len = newlen; + } + + public void append(final byte[] b, int off, int len) { + if (b == null) { + return; + } + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) < 0) || ((off + len) > b.length)) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + int oldlen = this.len; + int newlen = oldlen + len; + if (newlen > this.buffer.length) { + expand(newlen); + } + for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { + int ch = b[i1]; + if (ch < 0) { + ch = 256 + ch; + } + this.buffer[i2] = (char) ch; + } + this.len = newlen; + } + + public void append(final ByteArrayBuffer b, int off, int len) { + if (b == null) { + return; + } + append(b.buffer(), off, len); + } + + public void append(final Object obj) { + append(String.valueOf(obj)); + } + + public void clear() { + this.len = 0; + } + + public char[] toCharArray() { + char[] b = new char[this.len]; + if (this.len > 0) { + System.arraycopy(this.buffer, 0, b, 0, this.len); + } + return b; + } + + public char charAt(int i) { + return this.buffer[i]; + } + + public char[] buffer() { + return this.buffer; + } + + public int capacity() { + return this.buffer.length; + } + + public int length() { + return this.len; + } + + public void ensureCapacity(int required) { + int available = this.buffer.length - this.len; + if (required > available) { + expand(this.len + required); + } + } + + public void setLength(int len) { + if (len < 0 || len > this.buffer.length) { + throw new IndexOutOfBoundsException(); + } + this.len = len; + } + + public boolean isEmpty() { + return this.len == 0; + } + + public boolean isFull() { + return this.len == this.buffer.length; + } + + public int indexOf(int ch, int beginIndex, int endIndex) { + if (beginIndex < 0) { + beginIndex = 0; + } + if (endIndex > this.len) { + endIndex = this.len; + } + if (beginIndex > endIndex) { + return -1; + } + for (int i = beginIndex; i < endIndex; i++) { + if (this.buffer[i] == ch) { + return i; + } + } + return -1; + } + + public int indexOf(int ch) { + return indexOf(ch, 0, this.len); + } + + public String substring(int beginIndex, int endIndex) { + if (beginIndex < 0) { + throw new IndexOutOfBoundsException(); + } + if (endIndex > this.len) { + throw new IndexOutOfBoundsException(); + } + if (beginIndex > endIndex) { + throw new IndexOutOfBoundsException(); + } + return new String(this.buffer, beginIndex, endIndex - beginIndex); + } + + public String substringTrimmed(int beginIndex, int endIndex) { + if (beginIndex < 0) { + throw new IndexOutOfBoundsException(); + } + if (endIndex > this.len) { + throw new IndexOutOfBoundsException(); + } + if (beginIndex > endIndex) { + throw new IndexOutOfBoundsException(); + } + while (beginIndex < endIndex && MessageUtils.isWhitespace(this.buffer[beginIndex])) { + beginIndex++; + } + while (endIndex > beginIndex && MessageUtils.isWhitespace(this.buffer[endIndex - 1])) { + endIndex--; + } + return new String(this.buffer, beginIndex, endIndex - beginIndex); + } + + public String toString() { + return new String(this.buffer, 0, this.len); + } + +} diff --git a/src/main/java/org/apache/james/mime4j/Event.java b/src/main/java/org/apache/james/mime4j/Event.java index 1a6d96f..88dcd4d 100644 --- a/src/main/java/org/apache/james/mime4j/Event.java +++ b/src/main/java/org/apache/james/mime4j/Event.java @@ -13,6 +13,9 @@ public final class Event { public static final Event HEADERS_PREMATURE_END = new Event("Unexpected end of headers detected. " + "Higher level boundary detected or EOF reached."); + /** Indicates that unexpected end of headers detected.*/ + public static final Event INALID_HEADER + = new Event("Invalid header encountered"); private final String code; diff --git a/src/main/java/org/apache/james/mime4j/InputBuffer.java b/src/main/java/org/apache/james/mime4j/InputBuffer.java index d441cad..eb92ecf 100644 --- a/src/main/java/org/apache/james/mime4j/InputBuffer.java +++ b/src/main/java/org/apache/james/mime4j/InputBuffer.java @@ -124,8 +124,13 @@ public class InputBuffer { * Communications of the ACM . 33(8):132-142. *

*/ - public int indexOf(final byte[] pattern) { - int len = this.buflen - this.bufpos; + public int indexOf(final byte[] pattern, int off, int len) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern may not be null"); + } + if (off < this.bufpos || len < 0 || off + len > this.buflen) { + throw new IndexOutOfBoundsException(); + } if (len < pattern.length) { return -1; } @@ -163,18 +168,57 @@ public class InputBuffer { return -1; } - public int length() { - return this.buflen; + /** + * Implements quick search algorithm as published by + *

+ * SUNDAY D.M., 1990, + * A very fast substring search algorithm, + * Communications of the ACM . 33(8):132-142. + *

+ */ + public int indexOf(final byte[] pattern) { + return indexOf(pattern, this.bufpos, this.buflen - this.bufpos); + } + + public int indexOf(byte b, int off, int len) { + if (off < this.bufpos || len < 0 || off + len > this.buflen) { + throw new IndexOutOfBoundsException(); + } + for (int i = off; i < off + len; i++) { + if (this.buffer[i] == b) { + return i; + } + } + return -1; + } + + public int indexOf(byte b) { + return indexOf(b, this.bufpos, this.buflen - this.bufpos); } public byte charAt(int pos) { + if (pos < this.bufpos || pos > this.buflen) { + throw new IndexOutOfBoundsException(); + } return this.buffer[pos]; } + public byte[] buf() { + return this.buffer; + } + public int pos() { return this.bufpos; } + public int limit() { + return this.buflen; + } + + public int length() { + return this.buflen - this.bufpos; + } + public int skip(int n) { int chunk = Math.min(n, this.buflen - this.bufpos); this.bufpos += chunk; @@ -191,7 +235,7 @@ public class InputBuffer { buffer.append("[pos: "); buffer.append(this.bufpos); buffer.append("]"); - buffer.append("[len: "); + buffer.append("[limit: "); buffer.append(this.buflen); buffer.append("]"); buffer.append("["); diff --git a/src/main/java/org/apache/james/mime4j/MimeBoundaryInputStream.java b/src/main/java/org/apache/james/mime4j/MimeBoundaryInputStream.java index d489b33..9246d2d 100644 --- a/src/main/java/org/apache/james/mime4j/MimeBoundaryInputStream.java +++ b/src/main/java/org/apache/james/mime4j/MimeBoundaryInputStream.java @@ -20,7 +20,6 @@ package org.apache.james.mime4j; import java.io.IOException; -import java.io.InputStream; /** * Stream that constrains itself to a single MIME body part. @@ -29,7 +28,7 @@ import java.io.InputStream; * * @version $Id: MimeBoundaryInputStream.java,v 1.2 2004/11/29 13:15:42 ntherning Exp $ */ -public class MimeBoundaryInputStream extends InputStream { +public class MimeBoundaryInputStream extends BufferingInputStream { private final InputBuffer buffer; private final byte[] boundary; @@ -121,13 +120,59 @@ public class MimeBoundaryInputStream extends InputStream { int chunk = Math.min(len, limit - buffer.pos()); return buffer.read(b, off, chunk); } - + + public int readLine(final ByteArrayBuffer dst) throws IOException { + if (dst == null) { + throw new IllegalArgumentException("Destination buffer may not be null"); + } + if (completed) { + return -1; + } + if (endOfStream() && !hasData()) { + skipBoundary(); + return -1; + } + + int total = 0; + boolean found = false; + int bytesRead = 0; + while (!found) { + if (!hasData()) { + bytesRead = fillBuffer(); + if (!hasData() && endOfStream()) { + skipBoundary(); + 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 boolean endOfStream() { return eof || atBoundary; } private boolean hasData() { - return limit > buffer.pos() && limit < buffer.length(); + return limit > buffer.pos() && limit < buffer.limit(); } private int fillBuffer() throws IOException { @@ -149,9 +194,9 @@ public class MimeBoundaryInputStream extends InputStream { calculateBoundaryLen(); } else { if (eof) { - limit = buffer.length(); + limit = buffer.limit(); } else { - limit = buffer.length() - (boundary.length + 1); + limit = buffer.limit() - (boundary.length + 1); // \r\n + (boundary - one char) } } @@ -179,23 +224,15 @@ public class MimeBoundaryInputStream extends InputStream { if (!completed) { completed = true; buffer.skip(boundaryLen); - for (;;) { + for (;;) { if (buffer.length() > 1) { int ch1 = buffer.charAt(buffer.pos()); int ch2 = buffer.charAt(buffer.pos() + 1); if (ch1 == '-' && ch2 == '-') { this.lastPart = true; buffer.skip(2); - if (buffer.length() > 1) { - ch1 = buffer.charAt(buffer.pos()); - ch2 = buffer.charAt(buffer.pos() + 1); - if (ch1 == '\r' && ch2 == '\n') { - buffer.skip(2); - } - } - } else if (ch1 == '\r' && ch2 == '\n') { - buffer.skip(2); - } + } + skipLineDelimiter(); break; } else { fillBuffer(); @@ -206,6 +243,23 @@ public class MimeBoundaryInputStream extends InputStream { } } } + + private void skipLineDelimiter() { + int ch1 = 0; + int ch2 = 0; + int len = buffer.length(); + if (len > 0) { + ch1 = buffer.charAt(buffer.pos()); + } + if (len > 1) { + ch2 = buffer.charAt(buffer.pos() + 1); + } + if (ch1 == '\r' && ch2 == '\n') { + buffer.skip(2); + } else if (ch1 == '\n') { + buffer.skip(1); + } + } public boolean isLastPart() { return lastPart; diff --git a/src/main/java/org/apache/james/mime4j/MimeEntity.java b/src/main/java/org/apache/james/mime4j/MimeEntity.java index d31a453..6d8dc0c 100644 --- a/src/main/java/org/apache/james/mime4j/MimeEntity.java +++ b/src/main/java/org/apache/james/mime4j/MimeEntity.java @@ -18,12 +18,13 @@ public class MimeEntity extends AbstractEntity { */ private static final int T_IN_MESSAGE = -3; - private final InputBuffer inbuffer; + private final RootInputStream rootStream; private final InputStream rawStream; + private final InputBuffer inbuffer; private int recursionMode; private MimeBoundaryInputStream mimeStream; - private EOFSensitiveInputStream dataStream; + private BufferingInputStreamAdaptor dataStream; private boolean skipHeader; public MimeEntity( @@ -35,10 +36,11 @@ public class MimeEntity extends AbstractEntity { int endState, boolean maximalBodyDescriptor, boolean strictParsing) { - super(rootStream, parent, startState, endState, maximalBodyDescriptor, strictParsing); + super(parent, startState, endState, maximalBodyDescriptor, strictParsing); + this.rootStream = rootStream; this.inbuffer = inbuffer; this.rawStream = rawStream; - this.dataStream = new EOFSensitiveInputStream(rawStream); + this.dataStream = new BufferingInputStreamAdaptor(rawStream); this.skipHeader = false; } @@ -68,7 +70,11 @@ public class MimeEntity extends AbstractEntity { body.addField("Content-Type", contentType); } - protected InputStream getDataStream() { + protected int getLineNumber() { + return rootStream.getLineNumber(); + } + + protected BufferingInputStream getDataStream() { return dataStream; } @@ -85,9 +91,6 @@ public class MimeEntity extends AbstractEntity { state = EntityStates.T_START_HEADER; break; case EntityStates.T_START_HEADER: - initHeaderParsing(); - state = parseField() ? EntityStates.T_FIELD : EntityStates.T_END_HEADER; - break; case EntityStates.T_FIELD: state = parseField() ? EntityStates.T_FIELD : EntityStates.T_END_HEADER; break; @@ -160,12 +163,12 @@ public class MimeEntity extends AbstractEntity { private void createMimeStream() throws IOException { mimeStream = new MimeBoundaryInputStream(inbuffer, body.getBoundary()); - dataStream = new EOFSensitiveInputStream(mimeStream); + dataStream = new BufferingInputStreamAdaptor(mimeStream); } private void clearMimeStream() { mimeStream = null; - dataStream = new EOFSensitiveInputStream(rawStream); + dataStream = new BufferingInputStreamAdaptor(rawStream); } private void advanceToBoundary() throws IOException { @@ -181,12 +184,10 @@ public class MimeEntity extends AbstractEntity { InputStream instream; if (MimeUtil.isBase64Encoding(transferEncoding)) { log.debug("base64 encoded message/rfc822 detected"); - instream = new EOLConvertingInputStream( - new Base64InputStream(mimeStream)); + instream = new Base64InputStream(mimeStream); } else if (MimeUtil.isQuotedPrintableEncoded(transferEncoding)) { log.debug("quoted-printable encoded message/rfc822 detected"); - instream = new EOLConvertingInputStream( - new QuotedPrintableInputStream(mimeStream)); + instream = new QuotedPrintableInputStream(mimeStream); } else { instream = dataStream; } diff --git a/src/main/java/org/apache/james/mime4j/MimeTokenStream.java b/src/main/java/org/apache/james/mime4j/MimeTokenStream.java index 20d2d69..f3d9dd0 100644 --- a/src/main/java/org/apache/james/mime4j/MimeTokenStream.java +++ b/src/main/java/org/apache/james/mime4j/MimeTokenStream.java @@ -152,7 +152,7 @@ public class MimeTokenStream implements EntityStates, RecursionMode { inbuffer = new InputBuffer(rootInputStream, 4 * 1024); switch (recursionMode) { case M_RAW: - RawEntity rawentity = new RawEntity(new BufferingInputStream(inbuffer)); + RawEntity rawentity = new RawEntity(new BasicBufferingInputStream(inbuffer)); currentStateMachine = rawentity; break; case M_NO_RECURSE: @@ -161,7 +161,7 @@ public class MimeTokenStream implements EntityStates, RecursionMode { case M_RECURSE: MimeEntity mimeentity = new MimeEntity( rootInputStream, - new BufferingInputStream(inbuffer), + new BasicBufferingInputStream(inbuffer), inbuffer, null, T_START_MESSAGE, diff --git a/src/main/java/org/apache/james/mime4j/util/MessageUtils.java b/src/main/java/org/apache/james/mime4j/util/MessageUtils.java index ab47324..fc4a3b6 100644 --- a/src/main/java/org/apache/james/mime4j/util/MessageUtils.java +++ b/src/main/java/org/apache/james/mime4j/util/MessageUtils.java @@ -41,6 +41,11 @@ public final class MessageUtils { public static final String CRLF = "\r\n"; + public static final int CR = 13; // + public static final int LF = 10; // + public static final int SP = 32; // + public static final int HT = 9; // + public static boolean isASCII(char ch) { return (0xFF80 & ch) == 0; } @@ -57,4 +62,9 @@ public final class MessageUtils { } return true; } + + public static boolean isWhitespace(char ch) { + return ch == SP || ch == HT || ch == CR || ch == LF; + } + } diff --git a/src/test/java/org/apache/james/mime4j/BasicBufferingInputStreamTest.java b/src/test/java/org/apache/james/mime4j/BasicBufferingInputStreamTest.java new file mode 100644 index 0000000..cdfdca9 --- /dev/null +++ b/src/test/java/org/apache/james/mime4j/BasicBufferingInputStreamTest.java @@ -0,0 +1,144 @@ +/**************************************************************** + * 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import junit.framework.TestCase; + +public class BasicBufferingInputStreamTest extends TestCase { + + public void testBasicOperations() throws Exception { + String text = "ah blahblah"; + byte[] b1 = text.getBytes("US-ASCII"); + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(b1), 4096); + + BasicBufferingInputStream instream = new BasicBufferingInputStream(inbuffer); + + assertEquals((byte)'a', instream.read()); + assertEquals((byte)'h', instream.read()); + assertEquals((byte)' ', instream.read()); + + byte[] tmp1 = new byte[4]; + assertEquals(4, instream.read(tmp1)); + assertEquals(4, instream.read(tmp1)); + + assertEquals(-1, instream.read(tmp1)); + assertEquals(-1, instream.read(tmp1)); + assertEquals(-1, instream.read()); + assertEquals(-1, instream.read()); + } + + public void testBasicReadLine() throws Exception { + + String[] teststrs = new String[5]; + teststrs[0] = "Hello\r\n"; + teststrs[1] = "This string should be much longer than the size of the input buffer " + + "which is only 16 bytes for this test\r\n"; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 15; i++) { + sb.append("123456789 "); + } + sb.append("and stuff like that\r\n"); + teststrs[2] = sb.toString(); + teststrs[3] = "\r\n"; + teststrs[4] = "And goodbye\r\n"; + + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + + for (int i = 0; i < teststrs.length; i++) { + outstream.write(teststrs[i].getBytes("US-ASCII")); + } + byte[] raw = outstream.toByteArray(); + + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(raw), 16); + BasicBufferingInputStream instream = new BasicBufferingInputStream(inbuffer); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + for (int i = 0; i < teststrs.length; i++) { + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals(teststrs[i], s); + } + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + + public void testReadEmptyLine() throws Exception { + + String teststr = "\n\n\r\n\r\r\n\n\n\n\n\n"; + byte[] raw = teststr.getBytes("US-ASCII"); + + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(raw), 4); + BufferingInputStream instream = new BasicBufferingInputStream(inbuffer); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + +} diff --git a/src/test/java/org/apache/james/mime4j/BufferingInputStreamAdaptorTest.java b/src/test/java/org/apache/james/mime4j/BufferingInputStreamAdaptorTest.java new file mode 100644 index 0000000..ed62a6a --- /dev/null +++ b/src/test/java/org/apache/james/mime4j/BufferingInputStreamAdaptorTest.java @@ -0,0 +1,144 @@ +/**************************************************************** + * 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import junit.framework.TestCase; + +public class BufferingInputStreamAdaptorTest extends TestCase { + + public void testBasicOperations() throws Exception { + String text = "ah blahblah"; + byte[] b1 = text.getBytes("US-ASCII"); + + BufferingInputStreamAdaptor instream = new BufferingInputStreamAdaptor( + new ByteArrayInputStream(b1)); + + assertEquals((byte)'a', instream.read()); + assertEquals((byte)'h', instream.read()); + assertEquals((byte)' ', instream.read()); + + byte[] tmp1 = new byte[4]; + assertEquals(4, instream.read(tmp1)); + assertEquals(4, instream.read(tmp1)); + + assertEquals(-1, instream.read(tmp1)); + assertEquals(-1, instream.read(tmp1)); + assertEquals(-1, instream.read()); + assertEquals(-1, instream.read()); + } + + public void testBasicReadLine() throws Exception { + + String[] teststrs = new String[5]; + teststrs[0] = "Hello\r\n"; + teststrs[1] = "This string should be much longer than the size of the input buffer " + + "which is only 16 bytes for this test\r\n"; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 15; i++) { + sb.append("123456789 "); + } + sb.append("and stuff like that\r\n"); + teststrs[2] = sb.toString(); + teststrs[3] = "\r\n"; + teststrs[4] = "And goodbye\r\n"; + + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + + for (int i = 0; i < teststrs.length; i++) { + outstream.write(teststrs[i].getBytes("US-ASCII")); + } + byte[] raw = outstream.toByteArray(); + + BufferingInputStreamAdaptor instream = new BufferingInputStreamAdaptor( + new ByteArrayInputStream(raw)); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + for (int i = 0; i < teststrs.length; i++) { + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals(teststrs[i], s); + } + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + + public void testReadEmptyLine() throws Exception { + + String teststr = "\n\n\r\n\r\r\n\n\n\n\n\n"; + byte[] raw = teststr.getBytes("US-ASCII"); + + BufferingInputStreamAdaptor instream = new BufferingInputStreamAdaptor( + new ByteArrayInputStream(raw)); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + +} diff --git a/src/test/java/org/apache/james/mime4j/InputBufferTest.java b/src/test/java/org/apache/james/mime4j/InputBufferTest.java index 9619e18..c5951ee 100644 --- a/src/test/java/org/apache/james/mime4j/InputBufferTest.java +++ b/src/test/java/org/apache/james/mime4j/InputBufferTest.java @@ -25,6 +25,107 @@ import junit.framework.TestCase; public class InputBufferTest extends TestCase { + public void testInvalidInput() throws Exception { + String text = "blah blah yada yada"; + byte[] b1 = text.getBytes("US-ASCII"); + String pattern = "blah"; + byte[] b2 = pattern.getBytes("US-ASCII"); + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(b1), 4096); + inbuffer.fillBuffer(); + + assertEquals((int)'b', inbuffer.read()); + assertEquals((int)'l', inbuffer.read()); + + try { + inbuffer.charAt(1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.charAt(20); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf(b2, -1, 3); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf(b2, 1, 3); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf(b2, 2, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf(b2, 2, 18); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + assertEquals(5, inbuffer.indexOf(b2, 2, 17)); + try { + inbuffer.indexOf((byte)' ', -1, 3); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf((byte)' ', 1, 3); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf((byte)' ', 2, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + try { + inbuffer.indexOf((byte)' ', 2, 18); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException expected) { + } + assertEquals(10, inbuffer.indexOf((byte)'y', 2, 17)); + } + + public void testBasicOperations() throws Exception { + String text = "bla bla yada yada haha haha"; + byte[] b1 = text.getBytes("US-ASCII"); + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(b1), 4096); + inbuffer.fillBuffer(); + assertEquals(0, inbuffer.pos()); + assertEquals(27, inbuffer.limit()); + assertEquals(27, inbuffer.length()); + + inbuffer.read(); + inbuffer.read(); + + assertEquals(2, inbuffer.pos()); + assertEquals(27, inbuffer.limit()); + assertEquals(25, inbuffer.length()); + + byte[] tmp1 = new byte[3]; + assertEquals(3, inbuffer.read(tmp1)); + + assertEquals(5, inbuffer.pos()); + assertEquals(27, inbuffer.limit()); + assertEquals(22, inbuffer.length()); + + byte[] tmp2 = new byte[22]; + assertEquals(22, inbuffer.read(tmp2)); + + assertEquals(27, inbuffer.pos()); + assertEquals(27, inbuffer.limit()); + assertEquals(0, inbuffer.length()); + + assertEquals(-1, inbuffer.read(tmp1)); + assertEquals(-1, inbuffer.read(tmp1)); + assertEquals(-1, inbuffer.read()); + assertEquals(-1, inbuffer.read()); + } + public void testPatternMatching1() throws Exception { String text = "blabla d is the word"; String pattern = "d"; @@ -68,4 +169,32 @@ public class InputBufferTest extends TestCase { int i = inbuffer.indexOf(b2); assertEquals(0, i); } + + public void testPatternOutOfBound() throws Exception { + String text = "bla bla yada yada haha haha"; + String pattern1 = "bla bla"; + byte[] b1 = text.getBytes("US-ASCII"); + byte[] b2 = pattern1.getBytes("US-ASCII"); + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(b1), 4096); + inbuffer.fillBuffer(); + byte[] tmp = new byte[3]; + inbuffer.read(tmp); + int i = inbuffer.indexOf(b2, inbuffer.pos(), inbuffer.length()); + assertEquals(-1, i); + i = inbuffer.indexOf(b2, inbuffer.pos(), inbuffer.length() - 1); + assertEquals(-1, i); + } + + public void testCharOutOfBound() throws Exception { + String text = "zzz blah blah blah ggg"; + byte[] b1 = text.getBytes("US-ASCII"); + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(b1), 4096); + inbuffer.fillBuffer(); + byte[] tmp = new byte[3]; + inbuffer.read(tmp); + int i = inbuffer.indexOf((byte)'z', inbuffer.pos(), inbuffer.length()); + assertEquals(-1, i); + i = inbuffer.indexOf((byte)'g', inbuffer.pos(), inbuffer.length() - 3); + assertEquals(-1, i); + } } diff --git a/src/test/java/org/apache/james/mime4j/MimeBoundaryInputStreamTest.java b/src/test/java/org/apache/james/mime4j/MimeBoundaryInputStreamTest.java index ec5fab4..4f8c7ff 100644 --- a/src/test/java/org/apache/james/mime4j/MimeBoundaryInputStreamTest.java +++ b/src/test/java/org/apache/james/mime4j/MimeBoundaryInputStreamTest.java @@ -20,6 +20,7 @@ package org.apache.james.mime4j; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -55,6 +56,25 @@ public class MimeBoundaryInputStreamTest extends TestCase { assertTrue(mime2.isLastPart()); } + public void testLenientLineDelimiterReading() throws IOException { + String text = "Line 1\r\nLine 2\n--boundary\n" + + "Line 3\r\nLine 4\n--boundary--\n"; + + ByteArrayInputStream bis = new ByteArrayInputStream(text.getBytes("US-ASCII")); + + InputBuffer buffer = new InputBuffer(bis, 4096); + + MimeBoundaryInputStream mime1 = new MimeBoundaryInputStream(buffer, "boundary"); + assertEquals("Line 1\r\nLine 2", read(mime1, 5)); + + assertFalse(mime1.isLastPart()); + + MimeBoundaryInputStream mime2 = new MimeBoundaryInputStream(buffer, "boundary"); + assertEquals("Line 3\r\nLine 4", read(mime2, 5)); + + assertTrue(mime2.isLastPart()); + } + public void testBasicReadingSmallBuffer1() throws IOException { String text = "yadayadayadayadayadayadayadayadayadayadayadayadayadayadayadayada\r\n--boundary\r\n" + "blahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblahblah\r\n--boundary--"; @@ -215,5 +235,99 @@ public class MimeBoundaryInputStreamTest extends TestCase { buffer = new InputBuffer(bis, 4096); stream = new MimeBoundaryInputStream(buffer, "boundary"); assertEquals(-1, stream.read()); - } + } + + + public void testBasicReadLine() throws Exception { + + String[] teststrs = new String[5]; + teststrs[0] = "Hello\r\n"; + teststrs[1] = "This string should be much longer than the size of the input buffer " + + "which is only 20 bytes for this test\r\n"; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 15; i++) { + sb.append("123456789 "); + } + sb.append("and stuff like that\r\n"); + teststrs[2] = sb.toString(); + teststrs[3] = "\r\n"; + teststrs[4] = "And goodbye\r\n"; + + String term = "\r\n--1234\r\n"; + + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + + for (int i = 0; i < teststrs.length; i++) { + outstream.write(teststrs[i].getBytes("US-ASCII")); + } + outstream.write(term.getBytes("US-ASCII")); + byte[] raw = outstream.toByteArray(); + + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(raw), 20); + BufferingInputStream instream = new MimeBoundaryInputStream(inbuffer, "1234"); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + for (int i = 0; i < teststrs.length; i++) { + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals(teststrs[i], s); + } + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + + public void testReadEmptyLine() throws Exception { + + String teststr = "01234567890123456789\n\n\r\n\r\r\n\n\n\n\n\n--1234\r\n"; + byte[] raw = teststr.getBytes("US-ASCII"); + + InputBuffer inbuffer = new InputBuffer(new ByteArrayInputStream(raw), 20); + BufferingInputStream instream = new MimeBoundaryInputStream(inbuffer, "1234"); + + ByteArrayBuffer linebuf = new ByteArrayBuffer(8); + linebuf.clear(); + instream.readLine(linebuf); + String s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("01234567890123456789\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\r\r\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + linebuf.clear(); + instream.readLine(linebuf); + s = new String(linebuf.toByteArray(), "US-ASCII"); + assertEquals("\n", s); + + assertEquals(-1, instream.readLine(linebuf)); + assertEquals(-1, instream.readLine(linebuf)); + } + } diff --git a/src/test/java/org/apache/james/mime4j/MimeEntityTest.java b/src/test/java/org/apache/james/mime4j/MimeEntityTest.java index 8a43917..d5204df 100644 --- a/src/test/java/org/apache/james/mime4j/MimeEntityTest.java +++ b/src/test/java/org/apache/james/mime4j/MimeEntityTest.java @@ -40,7 +40,7 @@ public class MimeEntityTest extends TestCase { ByteArrayInputStream instream = new ByteArrayInputStream(raw); RootInputStream rootStream = new RootInputStream(instream); InputBuffer inbuffer = new InputBuffer(rootStream, 12); - BufferingInputStream rawstream = new BufferingInputStream(inbuffer); + BasicBufferingInputStream rawstream = new BasicBufferingInputStream(inbuffer); MimeEntity entity = new MimeEntity( rootStream, @@ -129,7 +129,7 @@ public class MimeEntityTest extends TestCase { ByteArrayInputStream instream = new ByteArrayInputStream(raw); RootInputStream rootStream = new RootInputStream(instream); InputBuffer inbuffer = new InputBuffer(rootStream, 24); - BufferingInputStream rawstream = new BufferingInputStream(inbuffer); + BasicBufferingInputStream rawstream = new BasicBufferingInputStream(inbuffer); MimeEntity entity = new MimeEntity( rootStream, @@ -244,7 +244,7 @@ public class MimeEntityTest extends TestCase { ByteArrayInputStream instream = new ByteArrayInputStream(raw); RootInputStream rootStream = new RootInputStream(instream); InputBuffer inbuffer = new InputBuffer(rootStream, 24); - BufferingInputStream rawstream = new BufferingInputStream(inbuffer); + BasicBufferingInputStream rawstream = new BasicBufferingInputStream(inbuffer); MimeEntity entity = new MimeEntity( rootStream, diff --git a/src/test/java/org/apache/james/mime4j/StrictMimeTokenStreamTest.java b/src/test/java/org/apache/james/mime4j/StrictMimeTokenStreamTest.java index a7a0848..f8ad235 100644 --- a/src/test/java/org/apache/james/mime4j/StrictMimeTokenStreamTest.java +++ b/src/test/java/org/apache/james/mime4j/StrictMimeTokenStreamTest.java @@ -35,6 +35,7 @@ public class StrictMimeTokenStreamTest extends TestCase { parser.parse(new ByteArrayInputStream(HEADER_ONLY.getBytes())); assertEquals("Headers start", MimeTokenStream.T_START_HEADER, parser.next()); + assertEquals("Field", MimeTokenStream.T_FIELD, parser.next()); try { parser.next(); fail("Expected exception to be thrown"); diff --git a/src/test/java/org/apache/james/mime4j/TestByteArrayBuffer.java b/src/test/java/org/apache/james/mime4j/TestByteArrayBuffer.java new file mode 100644 index 0000000..c5d872b --- /dev/null +++ b/src/test/java/org/apache/james/mime4j/TestByteArrayBuffer.java @@ -0,0 +1,229 @@ +/**************************************************************** + * 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; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link ByteArrayBuffer}. + */ +public class TestByteArrayBuffer extends TestCase { + + public void testConstructor() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(16); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + assertNotNull(buffer.buffer()); + assertEquals(16, buffer.buffer().length); + try { + new ByteArrayBuffer(-1); + fail("IllegalArgumentException should have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + public void testSimpleAppend() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(16); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + byte[] b1 = buffer.toByteArray(); + assertNotNull(b1); + assertEquals(0, b1.length); + assertTrue(buffer.isEmpty()); + assertFalse(buffer.isFull()); + + byte[] tmp = new byte[] { 1, 2, 3, 4}; + buffer.append(tmp, 0, tmp.length); + assertEquals(16, buffer.capacity()); + assertEquals(4, buffer.length()); + assertFalse(buffer.isEmpty()); + assertFalse(buffer.isFull()); + + byte[] b2 = buffer.toByteArray(); + assertNotNull(b2); + assertEquals(4, b2.length); + for (int i = 0; i < tmp.length; i++) { + assertEquals(tmp[i], b2[i]); + assertEquals(tmp[i], buffer.byteAt(i)); + } + buffer.clear(); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + assertTrue(buffer.isEmpty()); + assertFalse(buffer.isFull()); + } + + public void testExpandAppend() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + assertEquals(4, buffer.capacity()); + + byte[] tmp = new byte[] { 1, 2, 3, 4}; + buffer.append(tmp, 0, 2); + buffer.append(tmp, 0, 4); + buffer.append(tmp, 0, 0); + + assertEquals(8, buffer.capacity()); + assertEquals(6, buffer.length()); + + buffer.append(tmp, 0, 4); + + assertEquals(16, buffer.capacity()); + assertEquals(10, buffer.length()); + } + + public void testInvalidAppend() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + buffer.append((byte[])null, 0, 0); + + byte[] tmp = new byte[] { 1, 2, 3, 4}; + try { + buffer.append(tmp, -1, 0); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, 8); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 10, Integer.MAX_VALUE); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 2, 4); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + public void testAppendOneByte() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + assertEquals(4, buffer.capacity()); + + byte[] tmp = new byte[] { 1, 127, -1, -128, 1, -2}; + for (int i = 0; i < tmp.length; i++) { + buffer.append(tmp[i]); + } + assertEquals(8, buffer.capacity()); + assertEquals(6, buffer.length()); + + for (int i = 0; i < tmp.length; i++) { + assertEquals(tmp[i], buffer.byteAt(i)); + } + } + + public void testSetLength() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + buffer.setLength(2); + assertEquals(2, buffer.length()); + } + + public void testSetInvalidLength() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + try { + buffer.setLength(-2); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.setLength(200); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + public void testAppendCharArrayAsAscii() throws Exception { + String s1 = "stuff"; + String s2 = " and more stuff"; + char[] b1 = s1.toCharArray(); + char[] b2 = s2.toCharArray(); + + ByteArrayBuffer buffer = new ByteArrayBuffer(8); + buffer.append(b1, 0, b1.length); + buffer.append(b2, 0, b2.length); + + assertEquals(s1 + s2, new String(buffer.toByteArray(), "US-ASCII")); + } + + public void testAppendNullCharArray() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(8); + buffer.append((char[])null, 0, 0); + assertEquals(0, buffer.length()); + } + + public void testAppendEmptyCharArray() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(8); + buffer.append(new char[] {}, 0, 0); + assertEquals(0, buffer.length()); + } + + public void testInvalidAppendCharArrayAsAscii() throws Exception { + ByteArrayBuffer buffer = new ByteArrayBuffer(4); + buffer.append((char[])null, 0, 0); + + char[] tmp = new char[] { '1', '2', '3', '4'}; + try { + buffer.append(tmp, -1, 0); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, 8); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 10, Integer.MAX_VALUE); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 2, 4); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + +} diff --git a/src/test/java/org/apache/james/mime4j/TestCharArrayBuffer.java b/src/test/java/org/apache/james/mime4j/TestCharArrayBuffer.java new file mode 100644 index 0000000..d6cf5ba --- /dev/null +++ b/src/test/java/org/apache/james/mime4j/TestCharArrayBuffer.java @@ -0,0 +1,357 @@ +/**************************************************************** + * 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; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit tests for {@link CharArrayBuffer}. + */ +public class TestCharArrayBuffer extends TestCase { + + public TestCharArrayBuffer(String testName) { + super(testName); + } + + public static void main(String args[]) { + String[] testCaseName = { TestCharArrayBuffer.class.getName() }; + junit.textui.TestRunner.main(testCaseName); + } + + public static Test suite() { + return new TestSuite(TestCharArrayBuffer.class); + } + + public void testConstructor() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(16); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + assertNotNull(buffer.buffer()); + assertEquals(16, buffer.buffer().length); + try { + new CharArrayBuffer(-1); + fail("IllegalArgumentException should have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + public void testSimpleAppend() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(16); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + char[] b1 = buffer.toCharArray(); + assertNotNull(b1); + assertEquals(0, b1.length); + assertTrue(buffer.isEmpty()); + assertFalse(buffer.isFull()); + + char[] tmp = new char[] { '1', '2', '3', '4'}; + buffer.append(tmp, 0, tmp.length); + assertEquals(16, buffer.capacity()); + assertEquals(4, buffer.length()); + assertFalse(buffer.isEmpty()); + assertFalse(buffer.isFull()); + + char[] b2 = buffer.toCharArray(); + assertNotNull(b2); + assertEquals(4, b2.length); + for (int i = 0; i < tmp.length; i++) { + assertEquals(tmp[i], b2[i]); + assertEquals(tmp[i], buffer.charAt(i)); + } + assertEquals("1234", buffer.toString()); + + buffer.clear(); + assertEquals(16, buffer.capacity()); + assertEquals(0, buffer.length()); + assertTrue(buffer.isEmpty()); + assertFalse(buffer.isFull()); + } + + public void testExpandAppend() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + assertEquals(4, buffer.capacity()); + + char[] tmp = new char[] { '1', '2', '3', '4'}; + buffer.append(tmp, 0, 2); + buffer.append(tmp, 0, 4); + buffer.append(tmp, 0, 0); + + assertEquals(8, buffer.capacity()); + assertEquals(6, buffer.length()); + + buffer.append(tmp, 0, 4); + + assertEquals(16, buffer.capacity()); + assertEquals(10, buffer.length()); + + assertEquals("1212341234", buffer.toString()); + } + + public void testAppendString() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append("stuff"); + buffer.append(" and more stuff"); + assertEquals("stuff and more stuff", buffer.toString()); + } + + public void testAppendNullString() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append((String)null); + assertEquals("null", buffer.toString()); + } + + public void testAppendCharArrayBuffer() throws Exception { + CharArrayBuffer buffer1 = new CharArrayBuffer(8); + buffer1.append(" and more stuff"); + CharArrayBuffer buffer2 = new CharArrayBuffer(8); + buffer2.append("stuff"); + buffer2.append(buffer1); + assertEquals("stuff and more stuff", buffer2.toString()); + } + + public void testAppendNullCharArrayBuffer() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append((CharArrayBuffer)null); + buffer.append((CharArrayBuffer)null, 0, 0); + assertEquals("", buffer.toString()); + } + + public void testAppendSingleChar() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + buffer.append('1'); + buffer.append('2'); + buffer.append('3'); + buffer.append('4'); + buffer.append('5'); + buffer.append('6'); + assertEquals("123456", buffer.toString()); + } + + public void testInvalidCharArrayAppend() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + buffer.append((char[])null, 0, 0); + + char[] tmp = new char[] { '1', '2', '3', '4'}; + try { + buffer.append(tmp, -1, 0); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, 8); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 10, Integer.MAX_VALUE); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 2, 4); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + public void testSetLength() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + buffer.setLength(2); + assertEquals(2, buffer.length()); + } + + public void testSetInvalidLength() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + try { + buffer.setLength(-2); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.setLength(200); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + public void testEnsureCapacity() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + buffer.ensureCapacity(2); + assertEquals(4, buffer.capacity()); + buffer.ensureCapacity(8); + assertEquals(8, buffer.capacity()); + } + + public void testIndexOf() { + CharArrayBuffer buffer = new CharArrayBuffer(16); + buffer.append("name: value"); + assertEquals(4, buffer.indexOf(':')); + assertEquals(-1, buffer.indexOf(',')); + assertEquals(4, buffer.indexOf(':', -1, 11)); + assertEquals(4, buffer.indexOf(':', 0, 1000)); + assertEquals(-1, buffer.indexOf(':', 2, 1)); + } + + public void testSubstring() { + CharArrayBuffer buffer = new CharArrayBuffer(16); + buffer.append(" name: value "); + assertEquals(5, buffer.indexOf(':')); + assertEquals(" name", buffer.substring(0, 5)); + assertEquals(" value ", buffer.substring(6, buffer.length())); + assertEquals("name", buffer.substringTrimmed(0, 5)); + assertEquals("value", buffer.substringTrimmed(6, buffer.length())); + assertEquals("", buffer.substringTrimmed(13, buffer.length())); + } + + public void testSubstringIndexOfOutBound() { + CharArrayBuffer buffer = new CharArrayBuffer(16); + buffer.append("stuff"); + try { + buffer.substring(-2, 10); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.substringTrimmed(-2, 10); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.substring(12, 10); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.substringTrimmed(12, 10); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.substring(2, 1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.substringTrimmed(2, 1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + + public void testAppendAsciiByteArray() throws Exception { + String s1 = "stuff"; + String s2 = " and more stuff"; + byte[] b1 = s1.getBytes("US-ASCII"); + byte[] b2 = s2.getBytes("US-ASCII"); + + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append(b1, 0, b1.length); + buffer.append(b2, 0, b2.length); + + assertEquals("stuff and more stuff", buffer.toString()); + } + + public void testAppendISOByteArray() throws Exception { + byte[] b = new byte[] {0x00, 0x20, 0x7F, -0x80, -0x01}; + + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append(b, 0, b.length); + char[] ch = buffer.toCharArray(); + assertNotNull(ch); + assertEquals(5, ch.length); + assertEquals(0x00, ch[0]); + assertEquals(0x20, ch[1]); + assertEquals(0x7F, ch[2]); + assertEquals(0x80, ch[3]); + assertEquals(0xFF, ch[4]); + } + + public void testAppendNullByteArray() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append((byte[])null, 0, 0); + assertEquals("", buffer.toString()); + } + + public void testAppendNullByteArrayBuffer() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(8); + buffer.append((ByteArrayBuffer)null, 0, 0); + assertEquals("", buffer.toString()); + } + + public void testInvalidAppendAsciiByteArray() throws Exception { + CharArrayBuffer buffer = new CharArrayBuffer(4); + buffer.append((byte[])null, 0, 0); + + byte[] tmp = new byte[] { '1', '2', '3', '4'}; + try { + buffer.append(tmp, -1, 0); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, -1); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 0, 8); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 10, Integer.MAX_VALUE); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + try { + buffer.append(tmp, 2, 4); + fail("IndexOutOfBoundsException should have been thrown"); + } catch (IndexOutOfBoundsException ex) { + // expected + } + } + +} -- 2.11.4.GIT