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 }