MIME4J-27: Content length limitation Support

git-svn-id: https://svn.apache.org/repos/asf/james/mime4j/trunk@702290 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/james/mime4j/io/LimitedInputStream.java b/src/main/java/org/apache/james/mime4j/io/LimitedInputStream.java
new file mode 100644
index 0000000..020973b
--- /dev/null
+++ b/src/main/java/org/apache/james/mime4j/io/LimitedInputStream.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * 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 java.io.InputStream;
+import java.io.IOException;
+
+public class LimitedInputStream extends PositionInputStream {
+
+    private final long limit;
+
+    public LimitedInputStream(InputStream instream, long limit) {
+        super(instream);
+        if (limit < 0) {
+            throw new IllegalArgumentException("Limit may not be negative");
+        }
+        this.limit = limit;
+    }
+
+    private void enforceLimit() throws IOException {
+        if (position >= limit) {
+            throw new IOException("Input stream limit exceeded");
+        }
+    }
+    
+    public int read() throws IOException {
+        enforceLimit();
+        return super.read();
+    }
+
+    public int read(byte b[], int off, int len) throws IOException {
+        enforceLimit();
+        len = Math.min(len, getBytesLeft());
+        return super.read(b, off, len);
+    }
+
+    public long skip(long n) throws IOException {
+        enforceLimit();
+        n = Math.min(n, getBytesLeft());
+        return super.skip(n);
+    }
+
+    private int getBytesLeft() {
+        return (int)Math.min(Integer.MAX_VALUE, limit - position);
+    }
+    
+}
diff --git a/src/main/java/org/apache/james/mime4j/io/PartialInputStream.java b/src/main/java/org/apache/james/mime4j/io/PartialInputStream.java
index 9af4037..fe6bc60 100644
--- a/src/main/java/org/apache/james/mime4j/io/PartialInputStream.java
+++ b/src/main/java/org/apache/james/mime4j/io/PartialInputStream.java
@@ -45,12 +45,12 @@
 

     public int read(byte b[], int off, int len) throws IOException {

         len = Math.min(len, getBytesLeft());

-        return super.read(b, off, len);    //To change body of overridden methods use File | Settings | File Templates.

+        return super.read(b, off, len);

     }

 

     public long skip(long n) throws IOException {

         n = Math.min(n, getBytesLeft());

-        return super.skip(n);    //To change body of overridden methods use File | Settings | File Templates.

+        return super.skip(n);

     }

 

     private int getBytesLeft() {

diff --git a/src/main/java/org/apache/james/mime4j/io/PositionInputStream.java b/src/main/java/org/apache/james/mime4j/io/PositionInputStream.java
index 912742f..f858393 100644
--- a/src/main/java/org/apache/james/mime4j/io/PositionInputStream.java
+++ b/src/main/java/org/apache/james/mime4j/io/PositionInputStream.java
@@ -68,13 +68,15 @@
 

     public long skip(long n) throws IOException {

         final long c = in.skip(n);

-        position += c;

+        if (c > 0) 

+            position += c;

         return c;

     }

 

     public int read(byte b[], int off, int len) throws IOException {

         final int c = in.read(b, off, len);

-        position += c;

+        if (c > 0) 

+            position += c;

         return c;

     }

 

diff --git a/src/main/java/org/apache/james/mime4j/parser/MimeEntity.java b/src/main/java/org/apache/james/mime4j/parser/MimeEntity.java
index 798f2d6..e29dff0 100644
--- a/src/main/java/org/apache/james/mime4j/parser/MimeEntity.java
+++ b/src/main/java/org/apache/james/mime4j/parser/MimeEntity.java
@@ -27,6 +27,7 @@
 import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
 import org.apache.james.mime4j.descriptor.BodyDescriptor;
 import org.apache.james.mime4j.io.BufferedLineReaderInputStream;
+import org.apache.james.mime4j.io.LimitedInputStream;
 import org.apache.james.mime4j.io.LineReaderInputStream;
 import org.apache.james.mime4j.io.LineReaderInputStreamAdaptor;
 import org.apache.james.mime4j.io.MimeBoundaryInputStream;
@@ -228,7 +229,8 @@
             if (tmpbuf == null) {
                 tmpbuf = new byte[2048];
             }
-            while (dataStream.read(tmpbuf)!= -1) {
+            InputStream instream = getLimitedContentStream();
+            while (instream.read(tmpbuf)!= -1) {
             }
         }
     }
@@ -286,13 +288,22 @@
         }
     }
     
+    private InputStream getLimitedContentStream() {
+        long maxContentLimit = config.getMaxContentLen();
+        if (maxContentLimit >= 0) {
+            return new LimitedInputStream(dataStream, maxContentLimit);
+        } else {
+            return dataStream;
+        }
+    }
+    
     public InputStream getContentStream() {
         switch (state) {
         case EntityStates.T_START_MULTIPART:
         case EntityStates.T_PREAMBLE:
         case EntityStates.T_EPILOGUE:
         case EntityStates.T_BODY:
-            return this.dataStream;
+            return getLimitedContentStream();
         default:
             throw new IllegalStateException("Invalid state: " + stateToString(state));
         }
diff --git a/src/main/java/org/apache/james/mime4j/parser/MimeEntityConfig.java b/src/main/java/org/apache/james/mime4j/parser/MimeEntityConfig.java
index 13bf469..7e55453 100644
--- a/src/main/java/org/apache/james/mime4j/parser/MimeEntityConfig.java
+++ b/src/main/java/org/apache/james/mime4j/parser/MimeEntityConfig.java
@@ -31,6 +31,7 @@
     private boolean strictParsing;
     private int maxLineLen;
     private int maxHeaderCount;
+    private long maxContentLen;
     
     public MimeEntityConfig() {
         super();
@@ -38,6 +39,7 @@
         this.strictParsing = false;
         this.maxLineLen = 1000;
         this.maxHeaderCount = 1000;
+        this.maxContentLen = -1;
     }
     
     public boolean isMaximalBodyDescriptor() {
@@ -115,6 +117,28 @@
         return this.maxHeaderCount;
     }
 
+    /**
+     * Sets the maximum line length limit. Parsing of a MIME entity will be terminated 
+     * with a {@link MimeException} if a content body exceeds the maximum length limit. 
+     * If this parameter is set to a non positive value the content length
+     * check will be disabled.
+     * 
+     * @param maxLineLen maximum line length limit
+     */
+    public void setMaxContentLen(long maxContentLen) {
+        this.maxContentLen = maxContentLen;
+    }
+
+    /** 
+     * Returns the maximum content length limit
+     * @see #setMaxContentLen(long)
+     * 
+     * @return value of the the maximum content length limit
+     */
+    public long getMaxContentLen() {
+        return maxContentLen;
+    }
+
     public Object clone() throws CloneNotSupportedException {
         return super.clone();
     }
diff --git a/src/test/java/org/apache/james/mime4j/io/LimitedInputStreamTest.java b/src/test/java/org/apache/james/mime4j/io/LimitedInputStreamTest.java
new file mode 100644
index 0000000..6a79822
--- /dev/null
+++ b/src/test/java/org/apache/james/mime4j/io/LimitedInputStreamTest.java
@@ -0,0 +1,56 @@
+/****************************************************************
+ * 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+public class LimitedInputStreamTest extends TestCase {
+
+    public void testUpToLimitRead() throws IOException {
+        byte[] data = new byte[] {'0', '1', '2', '3', '4', '5', '6'};
+        ByteArrayInputStream instream = new ByteArrayInputStream(data);
+        LimitedInputStream limitedStream = new LimitedInputStream(instream, 3);
+        assertEquals(0, limitedStream.getPosition());
+        assertTrue(limitedStream.read() != -1);
+        assertEquals(1, limitedStream.getPosition());
+        byte[] tmp = new byte[3];
+        assertEquals(2, limitedStream.read(tmp));
+        assertEquals(3, limitedStream.getPosition());
+        try {
+            limitedStream.read();
+            fail("IOException should have been thrown");
+        } catch (IOException ex) {
+        }
+        try {
+            limitedStream.read(tmp);
+            fail("IOException should have been thrown");
+        } catch (IOException ex) {
+        }
+        try {
+            limitedStream.skip(2);
+            fail("IOException should have been thrown");
+        } catch (IOException ex) {
+        }
+    }
+    
+}
diff --git a/src/test/java/org/apache/james/mime4j/io/PositionInputStreamTest.java b/src/test/java/org/apache/james/mime4j/io/PositionInputStreamTest.java
new file mode 100644
index 0000000..62c5495
--- /dev/null
+++ b/src/test/java/org/apache/james/mime4j/io/PositionInputStreamTest.java
@@ -0,0 +1,53 @@
+/****************************************************************
+ * 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+public class PositionInputStreamTest extends TestCase {
+
+    public void testPositionCounting() throws IOException {
+        byte[] data = new byte[] {'0', '1', '2', '3', '4', '5', '6'};
+        ByteArrayInputStream instream = new ByteArrayInputStream(data);
+        PositionInputStream countingStream = new PositionInputStream(instream);
+        assertEquals(0, countingStream.getPosition());
+        assertTrue(countingStream.read() != -1);
+        assertEquals(1, countingStream.getPosition());
+        byte[] tmp = new byte[3];
+        assertEquals(3, countingStream.read(tmp));
+        assertEquals(4, countingStream.getPosition());
+        assertEquals(2, countingStream.skip(2));
+        assertEquals(6, countingStream.getPosition());
+        assertTrue(countingStream.read() != -1);
+        assertEquals(7, countingStream.getPosition());
+        assertTrue(countingStream.read() == -1);
+        assertEquals(7, countingStream.getPosition());
+        assertTrue(countingStream.read() == -1);
+        assertEquals(7, countingStream.getPosition());
+        assertTrue(countingStream.read(tmp) == -1);
+        assertEquals(7, countingStream.getPosition());
+        assertTrue(countingStream.read(tmp) == -1);
+        assertEquals(7, countingStream.getPosition());
+    }
+    
+}
diff --git a/src/test/java/org/apache/james/mime4j/parser/MimeEntityTest.java b/src/test/java/org/apache/james/mime4j/parser/MimeEntityTest.java
index c28bfac..00609a1 100644
--- a/src/test/java/org/apache/james/mime4j/parser/MimeEntityTest.java
+++ b/src/test/java/org/apache/james/mime4j/parser/MimeEntityTest.java
@@ -20,6 +20,7 @@
 package org.apache.james.mime4j.parser;

 

 import java.io.ByteArrayInputStream;

+import java.io.IOException;

 

 import org.apache.commons.io.IOUtils;

 import org.apache.james.mime4j.io.BufferedLineReaderInputStream;

@@ -483,4 +484,61 @@
         }

     }

 

+    public void testMaxContentLimitCheck() throws Exception {

+        String message = 

+            "To: Road Runner <runner@example.org>\r\n" +

+            "From: Wile E. Cayote <wile@example.org>\r\n" +

+            "Date: Tue, 12 Feb 2008 17:34:09 +0000 (GMT)\r\n" +

+            "Subject: Mail\r\n" +

+            "Content-Type: text/plain\r\n" +

+            "\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n" +

+            "DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS DoS\r\n";

+        byte[] raw = message.getBytes("US-ASCII");

+        ByteArrayInputStream instream = new ByteArrayInputStream(raw);

+        RootInputStream rootStream = new RootInputStream(instream); 

+        BufferedLineReaderInputStream rawstream = new BufferedLineReaderInputStream(rootStream, 12); 

+        

+        MimeEntityConfig config = new MimeEntityConfig();

+        config.setMaxContentLen(100);

+        MimeEntity entity = new MimeEntity(

+                rootStream,

+                rawstream,

+                null,

+                EntityStates.T_START_MESSAGE,

+                EntityStates.T_END_MESSAGE,

+                config);

+        

+        assertEquals(EntityStates.T_START_MESSAGE, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_START_HEADER, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_FIELD, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_FIELD, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_FIELD, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_FIELD, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_FIELD, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_END_HEADER, entity.getState());

+        entity.advance();

+        assertEquals(EntityStates.T_BODY, entity.getState());

+        try {

+            IOUtils.toByteArray(entity.getContentStream());

+            fail("IOException should have been thrown");

+        } catch (IOException expected) {

+        }

+    }

+    

 }