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}