View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  package org.apache.james.mime4j.io;
21  
22  import org.apache.james.mime4j.util.ByteArrayBuffer;
23  
24  import java.io.IOException;
25  
26  /**
27   * Stream that constrains itself to a single MIME body part.
28   * After the stream ends (i.e. read() returns -1) {@link #isLastPart()}
29   * can be used to determine if a final boundary has been seen or not.
30   */
31  public class MimeBoundaryInputStream extends LineReaderInputStream {
32  
33      private final byte[] boundary;
34      
35      private boolean eof;
36      private int limit;
37      private boolean atBoundary;
38      private int boundaryLen;
39      private boolean lastPart;
40      private boolean completed;
41  
42      private BufferedLineReaderInputStream buffer;
43  
44      /**
45       * Creates a new MimeBoundaryInputStream.
46       * 
47       * @param inbuffer The underlying stream.
48       * @param boundary Boundary string (not including leading hyphens).
49       * @throws IllegalArgumentException when boundary is too long
50       */
51      public MimeBoundaryInputStream(BufferedLineReaderInputStream inbuffer, String boundary) 
52              throws IOException {
53          super(inbuffer);
54          if (inbuffer.capacity() <= boundary.length()) {
55              throw new IllegalArgumentException("Boundary is too long");
56          }
57          this.buffer = inbuffer;
58          this.eof = false;
59          this.limit = -1;
60          this.atBoundary = false;
61          this.boundaryLen = 0;
62          this.lastPart = false;
63          this.completed = false;
64          
65          this.boundary = new byte[boundary.length() + 2];
66          this.boundary[0] = (byte) '-';
67          this.boundary[1] = (byte) '-';
68          for (int i = 0; i < boundary.length(); i++) {
69              byte ch = (byte) boundary.charAt(i);
70              if (ch == '\r' || ch == '\n') {
71                  throw new IllegalArgumentException("Boundary may not contain CR or LF");
72              }
73              this.boundary[i + 2] = ch;
74          }
75          fillBuffer();
76      }
77  
78      /**
79       * Closes the underlying stream.
80       * 
81       * @throws IOException on I/O errors.
82       */
83      @Override
84      public void close() throws IOException {
85      }
86  
87      /**
88       * @see java.io.InputStream#markSupported()
89       */
90      @Override
91      public boolean markSupported() {
92          return false;
93      }
94  
95      /**
96       * @see java.io.InputStream#read()
97       */
98      @Override
99      public int read() throws IOException {
100         if (completed) {
101             return -1;
102         }
103         if (endOfStream() && !hasData()) {
104             skipBoundary();            
105             return -1;
106         }
107         for (;;) {
108             if (hasData()) {
109                 return buffer.read();
110             } else if (endOfStream()) {
111                 skipBoundary();            
112                 return -1;
113             }
114             fillBuffer();
115         }
116     }
117     
118     @Override
119     public int read(byte[] b, int off, int len) throws IOException {
120         if (completed) {
121             return -1;
122         }
123         if (endOfStream() && !hasData()) {
124             skipBoundary();            
125             return -1;
126         }
127         fillBuffer();
128         if (!hasData()) {
129             return read(b, off, len);
130         }
131         int chunk = Math.min(len, limit - buffer.pos());
132         return buffer.read(b, off, chunk);
133     }
134 
135     @Override
136     public int readLine(final ByteArrayBuffer dst) throws IOException {
137         if (dst == null) {
138             throw new IllegalArgumentException("Destination buffer may not be null");
139         }
140         if (completed) {
141             return -1;
142         }
143         if (endOfStream() && !hasData()) {
144             skipBoundary();            
145             return -1;
146         }
147 
148         int total = 0;
149         boolean found = false;
150         int bytesRead = 0;
151         while (!found) {
152             if (!hasData()) {
153                 bytesRead = fillBuffer();
154                 if (!hasData() && endOfStream()) {
155                     skipBoundary();
156                     bytesRead = -1;
157                     break;
158                 }
159             }
160             int len = this.limit - this.buffer.pos();
161             int i = this.buffer.indexOf((byte)'\n', this.buffer.pos(), len);
162             int chunk;
163             if (i != -1) {
164                 found = true;
165                 chunk = i + 1 - this.buffer.pos();
166             } else {
167                 chunk = len;
168             }
169             if (chunk > 0) {
170                 dst.append(this.buffer.buf(), this.buffer.pos(), chunk);
171                 this.buffer.skip(chunk);
172                 total += chunk;
173             }
174         }
175         if (total == 0 && bytesRead == -1) {
176             return -1;
177         } else {
178             return total;
179         }
180     }
181     
182     private boolean endOfStream() {
183         return eof || atBoundary;
184     }
185     
186     private boolean hasData() {
187         return limit > buffer.pos() && limit <= buffer.limit();
188     }
189     
190     private int fillBuffer() throws IOException {
191         if (eof) {
192             return -1;
193         }
194         int bytesRead;
195         if (!hasData()) {
196             bytesRead = buffer.fillBuffer();
197         } else {
198             bytesRead = 0;
199         }
200         eof = bytesRead == -1;
201         
202         
203         int i = buffer.indexOf(boundary);
204         // NOTE this currently check only for LF. It doesn't check for canonical CRLF
205         // and neither for isolated CR. This will require updates according to MIME4J-60
206         while (i > 0 && buffer.charAt(i-1) != '\n') {
207             // skip the "fake" boundary (it does not contain LF or CR so we cannot have
208             // another boundary starting before this is complete.
209             i = i + boundary.length;
210             i = buffer.indexOf(boundary, i, buffer.limit() - i);
211         }
212         if (i != -1) {
213             limit = i;
214             atBoundary = true;
215             calculateBoundaryLen();
216         } else {
217             if (eof) {
218                 limit = buffer.limit();
219             } else {
220                 limit = buffer.limit() - (boundary.length + 1); 
221                                           // \r\n + (boundary - one char)
222             }
223         }
224         return bytesRead;
225     }
226     
227     private void calculateBoundaryLen() throws IOException {
228         boundaryLen = boundary.length;
229         int len = limit - buffer.pos();
230         if (len > 0) {
231             if (buffer.charAt(limit - 1) == '\n') {
232                 boundaryLen++;
233                 limit--;
234             }
235         }
236         if (len > 1) {
237             if (buffer.charAt(limit - 1) == '\r') {
238                 boundaryLen++;
239                 limit--;
240             }
241         }
242     }
243     
244     private void skipBoundary() throws IOException {
245         if (!completed) {
246             completed = true;
247             buffer.skip(boundaryLen);
248             boolean checkForLastPart = true;
249             for (;;) {
250                 if (buffer.length() > 1) {
251                     int ch1 = buffer.charAt(buffer.pos());
252                     int ch2 = buffer.charAt(buffer.pos() + 1);
253                     
254                     if (checkForLastPart) if (ch1 == '-' && ch2 == '-') {
255                         this.lastPart = true;
256                         buffer.skip(2);
257                         checkForLastPart = false;
258                         continue;
259                     }
260                     
261                     if (ch1 == '\r' && ch2 == '\n') {
262                         buffer.skip(2);
263                         break;
264                     } else if (ch1 == '\n') {
265                         buffer.skip(1);
266                         break;
267                     } else {
268                         // ignoring everything in a line starting with a boundary.
269                         buffer.skip(1);
270                     }
271                     
272                 } else {
273                     if (eof) {
274                         break;
275                     }
276                     fillBuffer();
277                 }
278             }
279         }
280     }
281     
282     public boolean isLastPart() {
283         return lastPart;        
284     }
285     
286     public boolean eof() {
287         return eof && !buffer.hasBufferedData();
288     }
289 
290     @Override
291     public String toString() {
292         final StringBuilder buffer = new StringBuilder("MimeBoundaryInputStream, boundary ");
293         for (byte b : boundary) {
294             buffer.append((char) b);
295         }
296         return buffer.toString();
297     }
298 }