pkg/mime: Removed mutable state from MimeMultipartTransformer

R=ajohnsen@google.com

Review URL: https://codereview.chromium.org//307963004

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/mime@36825 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/mime.dart b/lib/mime.dart
index 11b2ca9..dce6774 100644
--- a/lib/mime.dart
+++ b/lib/mime.dart
@@ -15,4 +15,5 @@
 library mime;
 
 export 'src/mime_multipart_transformer.dart';
+export 'src/mime_shared.dart';
 export 'src/mime_type.dart';
diff --git a/lib/src/bound_multipart_stream.dart b/lib/src/bound_multipart_stream.dart
new file mode 100644
index 0000000..d470034
--- /dev/null
+++ b/lib/src/bound_multipart_stream.dart
@@ -0,0 +1,375 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library mime.bound_multipart_stream;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'mime_shared.dart';
+import 'char_code.dart';
+
+// Bytes for '()<>@,;:\\"/[]?={} \t'.
+const _SEPARATORS = const [40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, 91, 93,
+                           63, 61, 123, 125, 32, 9];
+
+bool _isTokenChar(int byte) {
+  return byte > 31 && byte < 128 && _SEPARATORS.indexOf(byte) == -1;
+}
+
+int _toLowerCase(int byte) {
+  const delta = CharCode.LOWER_A - CharCode.UPPER_A;
+  return (CharCode.UPPER_A <= byte && byte <= CharCode.UPPER_Z) ?
+      byte + delta : byte;
+}
+
+void _expectByteValue(int val1, int val2) {
+  if (val1 != val2) {
+    throw new MimeMultipartException("Failed to parse multipart mime 1");
+  }
+}
+
+void _expectWhitespace(int byte) {
+  if (byte != CharCode.SP && byte != CharCode.HT) {
+    throw new MimeMultipartException("Failed to parse multipart mime 2");
+  }
+}
+
+class _MimeMultipart extends MimeMultipart {
+  final Map<String, String> headers;
+  final Stream<List<int>> _stream;
+
+  _MimeMultipart(this.headers, this._stream);
+
+  StreamSubscription<List<int>> listen(void onData(List<int> data),
+                                       {void onDone(),
+                                        Function onError,
+                                        bool cancelOnError}) {
+    return _stream.listen(onData,
+                          onDone: onDone,
+                          onError: onError,
+                          cancelOnError: cancelOnError);
+  }
+}
+
+class BoundMultipartStream {
+   static const int _START = 0;
+   static const int _FIRST_BOUNDARY_ENDING = 111;
+   static const int _FIRST_BOUNDARY_END = 112;
+   static const int _BOUNDARY_ENDING = 1;
+   static const int _BOUNDARY_END = 2;
+   static const int _HEADER_START = 3;
+   static const int _HEADER_FIELD = 4;
+   static const int _HEADER_VALUE_START = 5;
+   static const int _HEADER_VALUE = 6;
+   static const int _HEADER_VALUE_FOLDING_OR_ENDING = 7;
+   static const int _HEADER_VALUE_FOLD_OR_END = 8;
+   static const int _HEADER_ENDING = 9;
+   static const int _CONTENT = 10;
+   static const int _LAST_BOUNDARY_DASH2 = 11;
+   static const int _LAST_BOUNDARY_ENDING = 12;
+   static const int _LAST_BOUNDARY_END = 13;
+   static const int _DONE = 14;
+   static const int _FAIL = 15;
+
+   final List<int> _boundary;
+   final List<int> _headerField = [];
+   final List<int> _headerValue = [];
+
+   StreamController _controller;
+
+   Stream<MimeMultipart> get stream => _controller.stream;
+
+   StreamSubscription _subscription;
+
+   StreamController _multipartController;
+   Map<String, String> _headers;
+
+   int _state = _START;
+   int _boundaryIndex = 2;
+
+   // Current index in the data buffer. If index is negative then it
+   // is the index into the artificial prefix of the boundary string.
+   int _index;
+   List<int> _buffer;
+
+   BoundMultipartStream(this._boundary, Stream<List<int>> stream) {
+     _controller = new StreamController(
+         sync: true,
+         onPause: _pauseStream,
+         onResume:_resumeStream,
+         onCancel: () {
+           _subscription.cancel();
+         },
+         onListen: () {
+           _subscription = stream.listen(
+               (data) {
+                 assert(_buffer == null);
+                 _pauseStream();
+                 _buffer = data;
+                 _index = 0;
+                 _parse();
+               },
+               onDone: () {
+                 if (_state != _DONE) {
+                   _controller.addError(
+                       new MimeMultipartException("Bad multipart ending"));
+                 }
+                 _controller.close();
+               },
+               onError: _controller.addError);
+         });
+   }
+
+   void _resumeStream() {
+     _subscription.resume();
+   }
+
+   void _pauseStream() {
+     _subscription.pause();
+   }
+
+
+   void _parse() {
+     // Number of boundary bytes to artificially place before the supplied data.
+     int boundaryPrefix = 0;
+     // Position where content starts. Will be null if no known content
+     // start exists. Will be negative of the content starts in the
+     // boundary prefix. Will be zero or position if the content starts
+     // in the current buffer.
+     int contentStartIndex;
+
+     // Function to report content data for the current part. The data
+     // reported is from the current content start index up til the
+     // current index. As the data can be artificially prefixed with a
+     // prefix of the boundary both the content start index and index
+     // can be negative.
+     void reportData() {
+       if (contentStartIndex < 0) {
+         var contentLength = boundaryPrefix + _index - _boundaryIndex;
+         if (contentLength <= boundaryPrefix) {
+           _multipartController.add(
+               _boundary.sublist(0, contentLength));
+         } else {
+           _multipartController.add(
+               _boundary.sublist(0, boundaryPrefix));
+           _multipartController.add(
+               _buffer.sublist(0, contentLength - boundaryPrefix));
+         }
+       } else {
+         var contentEndIndex = _index - _boundaryIndex;
+         _multipartController.add(
+             _buffer.sublist(contentStartIndex, contentEndIndex));
+       }
+     }
+
+     if (_state == _CONTENT && _boundaryIndex == 0) {
+       contentStartIndex = 0;
+     } else {
+       contentStartIndex = null;
+     }
+     // The data to parse might be "artificially" prefixed with a
+     // partial match of the boundary.
+     boundaryPrefix = _boundaryIndex;
+
+     while ((_index < _buffer.length) && _state != _FAIL && _state != _DONE) {
+       if (_multipartController != null && _multipartController.isPaused) {
+         return;
+       }
+       int byte;
+       if (_index < 0) {
+         byte = _boundary[boundaryPrefix + _index];
+       } else {
+         byte = _buffer[_index];
+       }
+       switch (_state) {
+         case _START:
+           if (byte == _boundary[_boundaryIndex]) {
+             _boundaryIndex++;
+             if (_boundaryIndex == _boundary.length) {
+               _state = _FIRST_BOUNDARY_ENDING;
+               _boundaryIndex = 0;
+             }
+           } else {
+             // Restart matching of the boundary.
+             _index = _index - _boundaryIndex;
+             _boundaryIndex = 0;
+           }
+           break;
+
+         case _FIRST_BOUNDARY_ENDING:
+           if (byte == CharCode.CR) {
+             _state = _FIRST_BOUNDARY_END;
+           } else {
+             _expectWhitespace(byte);
+           }
+           break;
+
+         case _FIRST_BOUNDARY_END:
+           _expectByteValue(byte, CharCode.LF);
+           _state = _HEADER_START;
+           break;
+
+         case _BOUNDARY_ENDING:
+           if (byte == CharCode.CR) {
+             _state = _BOUNDARY_END;
+           } else if (byte == CharCode.DASH) {
+             _state = _LAST_BOUNDARY_DASH2;
+           } else {
+             _expectWhitespace(byte);
+           }
+           break;
+
+         case _BOUNDARY_END:
+           _expectByteValue(byte, CharCode.LF);
+           _multipartController.close();
+           _multipartController = null;
+           _state = _HEADER_START;
+           break;
+
+         case _HEADER_START:
+           _headers = new Map<String, String>();
+           if (byte == CharCode.CR) {
+             _state = _HEADER_ENDING;
+           } else {
+             // Start of new header field.
+             _headerField.add(_toLowerCase(byte));
+             _state = _HEADER_FIELD;
+           }
+           break;
+
+         case _HEADER_FIELD:
+           if (byte == CharCode.COLON) {
+             _state = _HEADER_VALUE_START;
+           } else {
+             if (!_isTokenChar(byte)) {
+               throw new MimeMultipartException("Invalid header field name");
+             }
+             _headerField.add(_toLowerCase(byte));
+           }
+           break;
+
+         case _HEADER_VALUE_START:
+           if (byte == CharCode.CR) {
+             _state = _HEADER_VALUE_FOLDING_OR_ENDING;
+           } else if (byte != CharCode.SP && byte != CharCode.HT) {
+             // Start of new header value.
+             _headerValue.add(byte);
+             _state = _HEADER_VALUE;
+           }
+           break;
+
+         case _HEADER_VALUE:
+           if (byte == CharCode.CR) {
+             _state = _HEADER_VALUE_FOLDING_OR_ENDING;
+           } else {
+             _headerValue.add(byte);
+           }
+           break;
+
+         case _HEADER_VALUE_FOLDING_OR_ENDING:
+           _expectByteValue(byte, CharCode.LF);
+           _state = _HEADER_VALUE_FOLD_OR_END;
+           break;
+
+         case _HEADER_VALUE_FOLD_OR_END:
+           if (byte == CharCode.SP || byte == CharCode.HT) {
+             _state = _HEADER_VALUE_START;
+           } else {
+             String headerField = UTF8.decode(_headerField);
+             String headerValue = UTF8.decode(_headerValue);
+             _headers[headerField.toLowerCase()] = headerValue;
+             _headerField.clear();
+             _headerValue.clear();
+             if (byte == CharCode.CR) {
+               _state = _HEADER_ENDING;
+             } else {
+               // Start of new header field.
+               _headerField.add(_toLowerCase(byte));
+               _state = _HEADER_FIELD;
+             }
+           }
+           break;
+
+         case _HEADER_ENDING:
+           _expectByteValue(byte, CharCode.LF);
+           _multipartController = new StreamController(
+               sync: true,
+               onPause: () {
+                 _pauseStream();
+               },
+               onResume: () {
+                 _resumeStream();
+                 _parse();
+               });
+           _controller.add(
+               new _MimeMultipart(_headers, _multipartController.stream));
+           _headers = null;
+           _state = _CONTENT;
+           contentStartIndex = _index + 1;
+           break;
+
+         case _CONTENT:
+           if (byte == _boundary[_boundaryIndex]) {
+             _boundaryIndex++;
+             if (_boundaryIndex == _boundary.length) {
+               if (contentStartIndex != null) {
+                 _index++;
+                 reportData();
+                 _index--;
+               }
+               _multipartController.close();
+               _boundaryIndex = 0;
+               _state = _BOUNDARY_ENDING;
+             }
+           } else {
+             // Restart matching of the boundary.
+             _index = _index - _boundaryIndex;
+             if (contentStartIndex == null) contentStartIndex = _index;
+             _boundaryIndex = 0;
+           }
+           break;
+
+         case _LAST_BOUNDARY_DASH2:
+           _expectByteValue(byte, CharCode.DASH);
+           _state = _LAST_BOUNDARY_ENDING;
+           break;
+
+         case _LAST_BOUNDARY_ENDING:
+           if (byte == CharCode.CR) {
+             _state = _LAST_BOUNDARY_END;
+           } else {
+             _expectWhitespace(byte);
+           }
+           break;
+
+         case _LAST_BOUNDARY_END:
+           _expectByteValue(byte, CharCode.LF);
+           _multipartController.close();
+           _multipartController = null;
+           _state = _DONE;
+           break;
+
+         default:
+           // Should be unreachable.
+           assert(false);
+           break;
+       }
+
+       // Move to the next byte.
+       _index++;
+     }
+
+     // Report any known content.
+     if (_state == _CONTENT && contentStartIndex != null) {
+       reportData();
+     }
+
+     // Resume if at end.
+     if (_index == _buffer.length) {
+       _buffer = null;
+       _index = null;
+       _resumeStream();
+     }
+   }
+}
diff --git a/lib/src/char_code.dart b/lib/src/char_code.dart
new file mode 100644
index 0000000..f455e68
--- /dev/null
+++ b/lib/src/char_code.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library mime.char_code;
+
+class CharCode {
+  static const int HT = 9;
+  static const int LF = 10;
+  static const int CR = 13;
+  static const int SP = 32;
+  static const int DASH = 45;
+  static const int COLON = 58;
+  static const int UPPER_A = 65;
+  static const int UPPER_Z = 90;
+  static const int LOWER_A = 97;
+}
diff --git a/lib/src/mime_multipart_transformer.dart b/lib/src/mime_multipart_transformer.dart
index 75d10ff..3afff2d 100644
--- a/lib/src/mime_multipart_transformer.dart
+++ b/lib/src/mime_multipart_transformer.dart
@@ -1,45 +1,28 @@
 // Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-
 library mime.multipart_transformer;
 
 import 'dart:async';
-import 'dart:convert';
 import 'dart:typed_data';
 
-/**
- * A Mime Multipart class representing each part parsed by
- * [MimeMultipartTransformer]. The data is streamed in as it become available.
- */
-abstract class MimeMultipart extends Stream<List<int>> {
-  Map<String, String> get headers;
-}
+import 'bound_multipart_stream.dart';
+import 'mime_shared.dart';
+import 'char_code.dart';
 
-class _MimeMultipart extends MimeMultipart {
-  final Map<String, String> headers;
-  final Stream<List<int>> _stream;
 
-  _MimeMultipart(this.headers, this._stream);
+Uint8List _getBoundary(String boundary) {
+  var charCodes = boundary.codeUnits;
 
-  StreamSubscription<List<int>> listen(void onData(List<int> data),
-                                       {void onDone(),
-                                        Function onError,
-                                        bool cancelOnError}) {
-    return _stream.listen(onData,
-                          onDone: onDone,
-                          onError: onError,
-                          cancelOnError: cancelOnError);
-  }
-}
-
-class _CharCode {
-  static const int HT = 9;
-  static const int LF = 10;
-  static const int CR = 13;
-  static const int SP = 32;
-  static const int DASH = 45;
-  static const int COLON = 58;
+  var boundaryList = new Uint8List(4 + charCodes.length);
+  // Set-up the matching boundary preceding it with CRLF and two
+  // dashes.
+  boundaryList[0] = CharCode.CR;
+  boundaryList[1] = CharCode.LF;
+  boundaryList[2] = CharCode.DASH;
+  boundaryList[3] = CharCode.DASH;
+  boundaryList.setRange(4, 4 + charCodes.length, charCodes);
+  return boundaryList;
 }
 
 /**
@@ -49,373 +32,18 @@
  */
 class MimeMultipartTransformer
     implements StreamTransformer<List<int>, MimeMultipart> {
-  static const int _START = 0;
-  static const int _FIRST_BOUNDARY_ENDING = 111;
-  static const int _FIRST_BOUNDARY_END = 112;
-  static const int _BOUNDARY_ENDING = 1;
-  static const int _BOUNDARY_END = 2;
-  static const int _HEADER_START = 3;
-  static const int _HEADER_FIELD = 4;
-  static const int _HEADER_VALUE_START = 5;
-  static const int _HEADER_VALUE = 6;
-  static const int _HEADER_VALUE_FOLDING_OR_ENDING = 7;
-  static const int _HEADER_VALUE_FOLD_OR_END = 8;
-  static const int _HEADER_ENDING = 9;
-  static const int _CONTENT = 10;
-  static const int _LAST_BOUNDARY_DASH2 = 11;
-  static const int _LAST_BOUNDARY_ENDING = 12;
-  static const int _LAST_BOUNDARY_END = 13;
-  static const int _DONE = 14;
-  static const int _FAILURE = 15;
 
-  // Bytes for '()<>@,;:\\"/[]?={} \t'.
-  static const _SEPARATORS = const [40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47,
-                                   91, 93, 63, 61, 123, 125, 32, 9];
-
-  StreamController _controller;
-  StreamSubscription _subscription;
-
-  StreamController _multipartController;
-  Map<String, String> _headers;
-
-  List<int> _boundary;
-  int _state = _START;
-  int _boundaryIndex = 2;
-
-  // Current index in the data buffer. If index is negative then it
-  // is the index into the artificial prefix of the boundary string.
-  int _index;
-  List<int> _buffer;
-
-  List<int> _headerField = [];
-  List<int> _headerValue = [];
+  final List<int> _boundary;
 
   /**
    * Construct a new MIME multipart parser with the boundary
    * [boundary]. The boundary should be as specified in the content
    * type parameter, that is without the -- prefix.
    */
-  MimeMultipartTransformer(String boundary) {
-    List<int> charCodes = boundary.codeUnits;
-    _boundary = new Uint8List(4 + charCodes.length);
-    // Set-up the matching boundary preceding it with CRLF and two
-    // dashes.
-    _boundary[0] = _CharCode.CR;
-    _boundary[1] = _CharCode.LF;
-    _boundary[2] = _CharCode.DASH;
-    _boundary[3] = _CharCode.DASH;
-    _boundary.setRange(4, 4 + charCodes.length, charCodes);
-  }
-
-  void _resumeStream() {
-    _subscription.resume();
-  }
-
-  void _pauseStream() {
-    _subscription.pause();
-  }
+  MimeMultipartTransformer(String boundary)
+      : _boundary = _getBoundary(boundary);
 
   Stream<MimeMultipart> bind(Stream<List<int>> stream) {
-    _controller = new StreamController(
-        sync: true,
-        onPause: _pauseStream,
-        onResume:_resumeStream,
-        onCancel: () {
-          _subscription.cancel();
-        },
-        onListen: () {
-          _subscription = stream.listen(
-              (data) {
-                assert(_buffer == null);
-                _pauseStream();
-                _buffer = data;
-                _index = 0;
-                _parse();
-              },
-              onDone: () {
-                if (_state != _DONE) {
-                  _controller.addError(
-                      new MimeMultipartException("Bad multipart ending"));
-                }
-                _controller.close();
-              },
-              onError: _controller.addError);
-        });
-    return _controller.stream;
+    return new BoundMultipartStream(_boundary, stream).stream;
   }
-
-  void _parse() {
-    // Number of boundary bytes to artificially place before the supplied data.
-    int boundaryPrefix = 0;
-    // Position where content starts. Will be null if no known content
-    // start exists. Will be negative of the content starts in the
-    // boundary prefix. Will be zero or position if the content starts
-    // in the current buffer.
-    int contentStartIndex;
-
-    // Function to report content data for the current part. The data
-    // reported is from the current content start index up til the
-    // current index. As the data can be artificially prefixed with a
-    // prefix of the boundary both the content start index and index
-    // can be negative.
-    void reportData() {
-      if (contentStartIndex < 0) {
-        var contentLength = boundaryPrefix + _index - _boundaryIndex;
-        if (contentLength <= boundaryPrefix) {
-          _multipartController.add(
-              _boundary.sublist(0, contentLength));
-        } else {
-          _multipartController.add(
-              _boundary.sublist(0, boundaryPrefix));
-          _multipartController.add(
-              _buffer.sublist(0, contentLength - boundaryPrefix));
-        }
-      } else {
-        var contentEndIndex = _index - _boundaryIndex;
-        _multipartController.add(
-            _buffer.sublist(contentStartIndex, contentEndIndex));
-      }
-    }
-
-    if (_state == _CONTENT && _boundaryIndex == 0) {
-      contentStartIndex = 0;
-    } else {
-      contentStartIndex = null;
-    }
-    // The data to parse might be "artificially" prefixed with a
-    // partial match of the boundary.
-    boundaryPrefix = _boundaryIndex;
-
-    while ((_index < _buffer.length) && _state != _FAILURE && _state != _DONE) {
-      if (_multipartController != null && _multipartController.isPaused) {
-        return;
-      }
-      int byte;
-      if (_index < 0) {
-        byte = _boundary[boundaryPrefix + _index];
-      } else {
-        byte = _buffer[_index];
-      }
-      switch (_state) {
-        case _START:
-          if (byte == _boundary[_boundaryIndex]) {
-            _boundaryIndex++;
-            if (_boundaryIndex == _boundary.length) {
-              _state = _FIRST_BOUNDARY_ENDING;
-              _boundaryIndex = 0;
-            }
-          } else {
-            // Restart matching of the boundary.
-            _index = _index - _boundaryIndex;
-            _boundaryIndex = 0;
-          }
-          break;
-
-        case _FIRST_BOUNDARY_ENDING:
-          if (byte == _CharCode.CR) {
-            _state = _FIRST_BOUNDARY_END;
-          } else {
-            _expectWS(byte);
-          }
-          break;
-
-        case _FIRST_BOUNDARY_END:
-          _expect(byte, _CharCode.LF);
-          _state = _HEADER_START;
-          break;
-
-        case _BOUNDARY_ENDING:
-          if (byte == _CharCode.CR) {
-            _state = _BOUNDARY_END;
-          } else if (byte == _CharCode.DASH) {
-            _state = _LAST_BOUNDARY_DASH2;
-          } else {
-            _expectWS(byte);
-          }
-          break;
-
-        case _BOUNDARY_END:
-          _expect(byte, _CharCode.LF);
-          _multipartController.close();
-          _multipartController = null;
-          _state = _HEADER_START;
-          break;
-
-        case _HEADER_START:
-          _headers = new Map<String, String>();
-          if (byte == _CharCode.CR) {
-            _state = _HEADER_ENDING;
-          } else {
-            // Start of new header field.
-            _headerField.add(_toLowerCase(byte));
-            _state = _HEADER_FIELD;
-          }
-          break;
-
-        case _HEADER_FIELD:
-          if (byte == _CharCode.COLON) {
-            _state = _HEADER_VALUE_START;
-          } else {
-            if (!_isTokenChar(byte)) {
-              throw new MimeMultipartException("Invalid header field name");
-            }
-            _headerField.add(_toLowerCase(byte));
-          }
-          break;
-
-        case _HEADER_VALUE_START:
-          if (byte == _CharCode.CR) {
-            _state = _HEADER_VALUE_FOLDING_OR_ENDING;
-          } else if (byte != _CharCode.SP && byte != _CharCode.HT) {
-            // Start of new header value.
-            _headerValue.add(byte);
-            _state = _HEADER_VALUE;
-          }
-          break;
-
-        case _HEADER_VALUE:
-          if (byte == _CharCode.CR) {
-            _state = _HEADER_VALUE_FOLDING_OR_ENDING;
-          } else {
-            _headerValue.add(byte);
-          }
-          break;
-
-        case _HEADER_VALUE_FOLDING_OR_ENDING:
-          _expect(byte, _CharCode.LF);
-          _state = _HEADER_VALUE_FOLD_OR_END;
-          break;
-
-        case _HEADER_VALUE_FOLD_OR_END:
-          if (byte == _CharCode.SP || byte == _CharCode.HT) {
-            _state = _HEADER_VALUE_START;
-          } else {
-            String headerField = UTF8.decode(_headerField);
-            String headerValue = UTF8.decode(_headerValue);
-            _headers[headerField.toLowerCase()] = headerValue;
-            _headerField = [];
-            _headerValue = [];
-            if (byte == _CharCode.CR) {
-              _state = _HEADER_ENDING;
-            } else {
-              // Start of new header field.
-              _headerField.add(_toLowerCase(byte));
-              _state = _HEADER_FIELD;
-            }
-          }
-          break;
-
-        case _HEADER_ENDING:
-          _expect(byte, _CharCode.LF);
-          _multipartController = new StreamController(
-              sync: true,
-              onPause: () {
-                _pauseStream();
-              },
-              onResume: () {
-                _resumeStream();
-                _parse();
-              });
-          _controller.add(
-              new _MimeMultipart(_headers, _multipartController.stream));
-          _headers = null;
-          _state = _CONTENT;
-          contentStartIndex = _index + 1;
-          break;
-
-        case _CONTENT:
-          if (byte == _boundary[_boundaryIndex]) {
-            _boundaryIndex++;
-            if (_boundaryIndex == _boundary.length) {
-              if (contentStartIndex != null) {
-                _index++;
-                reportData();
-                _index--;
-              }
-              _multipartController.close();
-              _boundaryIndex = 0;
-              _state = _BOUNDARY_ENDING;
-            }
-          } else {
-            // Restart matching of the boundary.
-            _index = _index - _boundaryIndex;
-            if (contentStartIndex == null) contentStartIndex = _index;
-            _boundaryIndex = 0;
-          }
-          break;
-
-        case _LAST_BOUNDARY_DASH2:
-          _expect(byte, _CharCode.DASH);
-          _state = _LAST_BOUNDARY_ENDING;
-          break;
-
-        case _LAST_BOUNDARY_ENDING:
-          if (byte == _CharCode.CR) {
-            _state = _LAST_BOUNDARY_END;
-          } else {
-            _expectWS(byte);
-          }
-          break;
-
-        case _LAST_BOUNDARY_END:
-          _expect(byte, _CharCode.LF);
-          _multipartController.close();
-          _multipartController = null;
-          _state = _DONE;
-          break;
-
-        default:
-          // Should be unreachable.
-          assert(false);
-          break;
-      }
-
-      // Move to the next byte.
-      _index++;
-    }
-
-    // Report any known content.
-    if (_state == _CONTENT && contentStartIndex != null) {
-      reportData();
-    }
-
-    // Resume if at end.
-    if (_index == _buffer.length) {
-      _buffer = null;
-      _index = null;
-      _resumeStream();
-    }
-  }
-
-  bool _isTokenChar(int byte) {
-    return byte > 31 && byte < 128 && _SEPARATORS.indexOf(byte) == -1;
-  }
-
-  int _toLowerCase(int byte) {
-    final int aCode = "A".codeUnitAt(0);
-    final int zCode = "Z".codeUnitAt(0);
-    final int delta = "a".codeUnitAt(0) - aCode;
-    return (aCode <= byte && byte <= zCode) ? byte + delta : byte;
-  }
-
-  void _expect(int val1, int val2) {
-    if (val1 != val2) {
-      throw new MimeMultipartException("Failed to parse multipart mime 1");
-    }
-  }
-
-  void _expectWS(int byte) {
-    if (byte != _CharCode.SP && byte != _CharCode.HT) {
-      throw new MimeMultipartException("Failed to parse multipart mime 2");
-    }
-  }
-}
-
-
-class MimeMultipartException implements Exception {
-  final String message;
-
-  const MimeMultipartException([String this.message = ""]);
-
-  String toString() => "MimeMultipartException: $message";
 }
diff --git a/lib/src/mime_shared.dart b/lib/src/mime_shared.dart
new file mode 100644
index 0000000..6d14e0a
--- /dev/null
+++ b/lib/src/mime_shared.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+library mime.shared;
+
+import 'dart:async';
+
+class MimeMultipartException implements Exception {
+  final String message;
+
+  const MimeMultipartException([String this.message = ""]);
+
+  String toString() => "MimeMultipartException: $message";
+}
+
+/**
+ * A Mime Multipart class representing each part parsed by
+ * [MimeMultipartTransformer]. The data is streamed in as it become available.
+ */
+abstract class MimeMultipart extends Stream<List<int>> {
+  Map<String, String> get headers;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 54dc5a8..7051582 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,9 +1,9 @@
 name: mime
-version: 0.9.0+2
+version: 0.9.0+3
 author: Dart Team <misc@dartlang.org>
 description: Helper-package for working with MIME.
 homepage: http://www.dartlang.org
 environment:
   sdk: '>=1.0.0 <2.0.0'
 dev_dependencies:
-  unittest: '>=0.9.0 <0.11.0'
+  unittest: '>=0.9.0 <0.12.0'