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.util.ByteArrayBuffer;
023    
024    import java.io.IOException;
025    
026    /**
027     * Stream that constrains itself to a single MIME body part.
028     * After the stream ends (i.e. read() returns -1) {@link #isLastPart()}
029     * can be used to determine if a final boundary has been seen or not.
030     */
031    public class MimeBoundaryInputStream extends LineReaderInputStream {
032    
033        private final byte[] boundary;
034        
035        private boolean eof;
036        private int limit;
037        private boolean atBoundary;
038        private int boundaryLen;
039        private boolean lastPart;
040        private boolean completed;
041    
042        private BufferedLineReaderInputStream buffer;
043    
044        /**
045         * Creates a new MimeBoundaryInputStream.
046         * 
047         * @param inbuffer The underlying stream.
048         * @param boundary Boundary string (not including leading hyphens).
049         * @throws IllegalArgumentException when boundary is too long
050         */
051        public MimeBoundaryInputStream(BufferedLineReaderInputStream inbuffer, String boundary) 
052                throws IOException {
053            super(inbuffer);
054            if (inbuffer.capacity() <= boundary.length()) {
055                throw new IllegalArgumentException("Boundary is too long");
056            }
057            this.buffer = inbuffer;
058            this.eof = false;
059            this.limit = -1;
060            this.atBoundary = false;
061            this.boundaryLen = 0;
062            this.lastPart = false;
063            this.completed = false;
064            
065            this.boundary = new byte[boundary.length() + 2];
066            this.boundary[0] = (byte) '-';
067            this.boundary[1] = (byte) '-';
068            for (int i = 0; i < boundary.length(); i++) {
069                byte ch = (byte) boundary.charAt(i);
070                if (ch == '\r' || ch == '\n') {
071                    throw new IllegalArgumentException("Boundary may not contain CR or LF");
072                }
073                this.boundary[i + 2] = ch;
074            }
075            fillBuffer();
076        }
077    
078        /**
079         * Closes the underlying stream.
080         * 
081         * @throws IOException on I/O errors.
082         */
083        @Override
084        public void close() throws IOException {
085        }
086    
087        /**
088         * @see java.io.InputStream#markSupported()
089         */
090        @Override
091        public boolean markSupported() {
092            return false;
093        }
094    
095        /**
096         * @see java.io.InputStream#read()
097         */
098        @Override
099        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    }