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    
020    package org.apache.james.mime4j.io;
021    
022    import org.apache.james.mime4j.MimeException;
023    import org.apache.james.mime4j.MimeIOException;
024    import org.apache.james.mime4j.util.ByteArrayBuffer;
025    import org.apache.james.mime4j.util.CharsetUtil;
026    
027    import 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     */
034    public 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    }