001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.mime4j.io;
021
022import org.apache.james.mime4j.MimeException;
023import org.apache.james.mime4j.MimeIOException;
024import org.apache.james.mime4j.util.ByteArrayBuffer;
025import org.apache.james.mime4j.util.CharsetUtil;
026
027import java.io.IOException;
028
029/**
030 * Stream that constrains itself to a single MIME body part.
031 * After the stream ends (i.e. read() returns -1) {@link #isLastPart()}
032 * can be used to determine if a final boundary has been seen or not.
033 */
034public class MimeBoundaryInputStream extends LineReaderInputStream {
035
036    private final byte[] boundary;
037    private final boolean strict;
038
039    private boolean eof;
040    private int limit;
041    private boolean atBoundary;
042    private int boundaryLen;
043    private boolean lastPart;
044    private boolean completed;
045
046    private BufferedLineReaderInputStream buffer;
047
048    /**
049     * Store the first buffer length.
050     * Used to distinguish between an empty preamble and
051     * no preamble.
052     */
053    private int initialLength;
054
055    /**
056     * Creates a new MimeBoundaryInputStream.
057     *
058     * @param inbuffer The underlying stream.
059     * @param boundary Boundary string (not including leading hyphens).
060     * @throws IllegalArgumentException when boundary is too long
061     */
062    public MimeBoundaryInputStream(
063            final BufferedLineReaderInputStream inbuffer,
064            final String boundary,
065            final boolean strict) throws IOException {
066        super(inbuffer);
067        int bufferSize = 2 * boundary.length();
068        if (bufferSize < 4096) {
069            bufferSize = 4096;
070        }
071        inbuffer.ensureCapacity(bufferSize);
072        this.buffer = inbuffer;
073        this.eof = false;
074        this.limit = -1;
075        this.atBoundary = false;
076        this.boundaryLen = 0;
077        this.lastPart = false;
078        this.initialLength = -1;
079        this.completed = false;
080
081        this.strict = strict;
082        this.boundary = new byte[boundary.length() + 2];
083        this.boundary[0] = (byte) '-';
084        this.boundary[1] = (byte) '-';
085        for (int i = 0; i < boundary.length(); i++) {
086            byte ch = (byte) boundary.charAt(i);
087            this.boundary[i + 2] = ch;
088        }
089
090        fillBuffer();
091    }
092
093    /**
094     * Creates a new MimeBoundaryInputStream.
095     *
096     * @param inbuffer The underlying stream.
097     * @param boundary Boundary string (not including leading hyphens).
098     * @throws IllegalArgumentException when boundary is too long
099     */
100    public MimeBoundaryInputStream(
101            final BufferedLineReaderInputStream inbuffer,
102            final String boundary) throws IOException {
103        this(inbuffer, boundary, false);
104    }
105
106    /**
107     * Closes the underlying stream.
108     *
109     * @throws IOException on I/O errors.
110     */
111    @Override
112    public void close() throws IOException {
113    }
114
115    /**
116     * @see java.io.InputStream#markSupported()
117     */
118    @Override
119    public boolean markSupported() {
120        return false;
121    }
122
123    public boolean readAllowed() throws IOException {
124        if (completed) {
125            return false;
126        }
127        if (endOfStream() && !hasData()) {
128            skipBoundary();
129            verifyEndOfStream();
130            return false;
131        }
132        return true;
133    }
134
135    /**
136     * @see java.io.InputStream#read()
137     */
138    @Override
139    public int read() throws IOException {
140        for (;;) {
141            if (!readAllowed()) return -1;
142            if (hasData()) {
143                return buffer.read();
144            }
145            fillBuffer();
146        }
147    }
148
149    @Override
150    public int read(byte[] b, int off, int len) throws IOException {
151        for (;;) {
152            if (!readAllowed()) return -1;
153            if (hasData()) {
154                int chunk = Math.min(len, limit - buffer.pos());
155                return buffer.read(b, off, chunk);
156            }
157            fillBuffer();
158        }
159    }
160
161    @Override
162    public int readLine(final ByteArrayBuffer dst) throws IOException {
163        if (dst == null) {
164            throw new IllegalArgumentException("Destination buffer may not be null");
165        }
166        if (!readAllowed()) return -1;
167
168        int total = 0;
169        boolean found = false;
170        int bytesRead = 0;
171        while (!found) {
172            if (!hasData()) {
173                bytesRead = fillBuffer();
174                if (endOfStream() && !hasData()) {
175                    skipBoundary();
176                    verifyEndOfStream();
177                    bytesRead = -1;
178                    break;
179                }
180            }
181            int len = this.limit - this.buffer.pos();
182            int i = this.buffer.indexOf((byte)'\n', this.buffer.pos(), len);
183            int chunk;
184            if (i != -1) {
185                found = true;
186                chunk = i + 1 - this.buffer.pos();
187            } else {
188                chunk = len;
189            }
190            if (chunk > 0) {
191                dst.append(this.buffer.buf(), this.buffer.pos(), chunk);
192                this.buffer.skip(chunk);
193                total += chunk;
194            }
195        }
196        if (total == 0 && bytesRead == -1) {
197            return -1;
198        } else {
199            return total;
200        }
201    }
202
203    private void verifyEndOfStream() throws IOException {
204        if (strict && eof && !atBoundary) {
205            throw new MimeIOException(new MimeException("Unexpected end of stream"));
206        }
207    }
208
209    private boolean endOfStream() {
210        return eof || atBoundary;
211    }
212
213    private boolean hasData() {
214        return limit > buffer.pos() && limit <= buffer.limit();
215    }
216
217    private int fillBuffer() throws IOException {
218        if (eof) {
219            return -1;
220        }
221        int bytesRead;
222        if (!hasData()) {
223            bytesRead = buffer.fillBuffer();
224            if (bytesRead == -1) {
225                eof = true;
226            }
227        } else {
228            bytesRead = 0;
229        }
230
231        int i;
232        int off = buffer.pos();
233        for (;;) {
234            i = buffer.indexOf(boundary, off, buffer.limit() - off);
235            if (i == -1) {
236                break;
237            }
238            // Make sure the boundary is either at the very beginning of the buffer
239            // or preceded with LF
240            if (i == buffer.pos() || buffer.byteAt(i - 1) == '\n') {
241                int pos = i + boundary.length;
242                int remaining = buffer.limit() - pos;
243                if (remaining <= 0) {
244                    // Make sure the boundary is terminated with EOS
245                    break;
246                } else {
247                    // or with a whitespace or '-' char
248                    char ch = (char)(buffer.byteAt(pos));
249                    if (CharsetUtil.isWhitespace(ch) || ch == '-') {
250                        break;
251                    }
252                }
253            }
254            off = i + boundary.length;
255        }
256        if (i != -1) {
257            limit = i;
258            atBoundary = true;
259            calculateBoundaryLen();
260        } else {
261            if (eof) {
262                limit = buffer.limit();
263            } else {
264                limit = buffer.limit() - (boundary.length + 2);
265                                // [LF] [boundary] [CR][LF] minus one char
266            }
267        }
268        return bytesRead;
269    }
270
271    public boolean isEmptyStream() {
272        return initialLength == 0;
273    }
274
275    public boolean isFullyConsumed() {
276        return completed && !buffer.hasBufferedData();
277    }
278
279    private void calculateBoundaryLen() throws IOException {
280        boundaryLen = boundary.length;
281        int len = limit - buffer.pos();
282        if (len >= 0 && initialLength == -1) initialLength = len;
283        if (len > 0) {
284            if (buffer.byteAt(limit - 1) == '\n') {
285                boundaryLen++;
286                limit--;
287            }
288        }
289        if (len > 1) {
290            if (buffer.byteAt(limit - 1) == '\r') {
291                boundaryLen++;
292                limit--;
293            }
294        }
295    }
296
297    private void skipBoundary() throws IOException {
298        if (!completed) {
299            completed = true;
300            buffer.skip(boundaryLen);
301            boolean checkForLastPart = true;
302            for (;;) {
303                if (buffer.length() > 1) {
304                    int ch1 = buffer.byteAt(buffer.pos());
305                    int ch2 = buffer.byteAt(buffer.pos() + 1);
306
307                    if (checkForLastPart) if (ch1 == '-' && ch2 == '-') {
308                        this.lastPart = true;
309                        buffer.skip(2);
310                        checkForLastPart = false;
311                        continue;
312                    }
313
314                    if (ch1 == '\r' && ch2 == '\n') {
315                        buffer.skip(2);
316                        break;
317                    } else if (ch1 == '\n') {
318                        buffer.skip(1);
319                        break;
320                    } else {
321                        // ignoring everything in a line starting with a boundary.
322                        buffer.skip(1);
323                    }
324
325                } else {
326                    if (eof) {
327                        break;
328                    }
329                    fillBuffer();
330                }
331            }
332        }
333    }
334
335    public boolean isLastPart() {
336        return lastPart;
337    }
338
339    public boolean eof() {
340        return eof && !buffer.hasBufferedData();
341    }
342
343    @Override
344    public String toString() {
345        final StringBuilder buffer = new StringBuilder("MimeBoundaryInputStream, boundary ");
346        for (byte b : boundary) {
347            buffer.append((char) b);
348        }
349        return buffer.toString();
350    }
351
352    @Override
353    public boolean unread(ByteArrayBuffer buf) {
354        return false;
355    }
356}