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 }