Move MimeMultipartTransformer and HttpBodyHandler to mime and http_server packages.

BUG=
R=whesse@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/mime@24991 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/mime.dart b/lib/mime.dart
index 0943de8..44c6cad 100644
--- a/lib/mime.dart
+++ b/lib/mime.dart
@@ -4,6 +4,10 @@
 
 library mime;
 
+import 'dart:async';
+import 'dart:typed_data';
+
 part 'src/mime_type.dart';
 part 'src/extension_map.dart';
 part 'src/magic_number.dart';
+part 'src/mime_multipart_transformer.dart';
diff --git a/lib/src/mime_multipart_transformer.dart b/lib/src/mime_multipart_transformer.dart
new file mode 100644
index 0000000..5813087
--- /dev/null
+++ b/lib/src/mime_multipart_transformer.dart
@@ -0,0 +1,420 @@
+// 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.
+
+part of mime;
+
+
+/**
+ * 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;
+}
+
+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(),
+                                        void onError(error),
+                                        bool cancelOnError}) {
+    return _stream.listen(onData,
+                          onDone: onDone,
+                          onError: onError,
+                          cancelOnError: cancelOnError);
+  }
+}
+
+class _Const {
+  // 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];
+}
+
+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;
+}
+
+/**
+ * Parser for MIME multipart types of data as described in RFC 2046
+ * section 5.1.1. The data is transformed into [MimeMultipart] objects, each
+ * of them streaming the multipart data.
+ */
+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;
+
+  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;
+
+  StringBuffer _headerField = new StringBuffer();
+  StringBuffer _headerValue = new StringBuffer();
+
+  /**
+   * 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();
+  }
+
+  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: (error) {
+                _controller.addError(error);
+              });
+        });
+    return _controller.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.writeCharCode(_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.writeCharCode(_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.writeCharCode(byte);
+            _state = _HEADER_VALUE;
+          }
+          break;
+
+        case _HEADER_VALUE:
+          if (byte == _CharCode.CR) {
+            _state = _HEADER_VALUE_FOLDING_OR_ENDING;
+          } else {
+            _headerValue.writeCharCode(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 = _headerField.toString();
+            String headerValue =_headerValue.toString();
+            _headers[headerField.toLowerCase()] = headerValue;
+            _headerField = new StringBuffer();
+            _headerValue = new StringBuffer();
+            if (byte == _CharCode.CR) {
+              _state = _HEADER_ENDING;
+            } else {
+              // Start of new header field.
+              _headerField.writeCharCode(_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 && _Const.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 {
+  const MimeMultipartException([String this.message = ""]);
+  String toString() => "MimeMultipartException: $message";
+  final String message;
+}
diff --git a/test/mime_multipart_transformer_test.dart b/test/mime_multipart_transformer_test.dart
new file mode 100644
index 0000000..f0dd91e
--- /dev/null
+++ b/test/mime_multipart_transformer_test.dart
@@ -0,0 +1,321 @@
+// Copyright (c) 2013, 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.
+
+import "package:unittest/unittest.dart";
+import "package:mime/mime.dart";
+import 'dart:async';
+import 'dart:math';
+import 'dart:isolate';
+
+void testParse(String message,
+               String boundary,
+               [List<Map> expectedHeaders,
+                List expectedParts,
+                bool expectError = false]) {
+  Future testWrite(List<int> data, [int chunkSize = -1]) {
+    StreamController controller = new StreamController(sync: true);
+
+    var stream = controller.stream.transform(
+        new MimeMultipartTransformer(boundary));
+    int i = 0;
+    var completer = new Completer();
+    var futures = [];
+    stream.listen((multipart) {
+      int part = i++;
+      if (expectedHeaders != null) {
+        expect(multipart.headers, equals(expectedHeaders[part]));
+      }
+      futures.add(multipart.fold([], (buffer, data) => buffer..addAll(data))
+          .then((data) {
+            if (expectedParts[part] != null) {
+              expect(data, equals(expectedParts[part].codeUnits));
+            }
+          }));
+    }, onError: (error) {
+      if (!expectError) throw error;
+    }, onDone: () {
+      if (expectedParts != null) {
+        expect(i, equals(expectedParts.length));
+      }
+      Future.wait(futures).then(completer.complete);
+    });
+
+    if (chunkSize == -1) chunkSize = data.length;
+
+    int written = 0;
+    for (int pos = 0; pos < data.length; pos += chunkSize) {
+      int remaining = data.length - pos;
+      int writeLength = min(chunkSize, remaining);
+      controller.add(data.sublist(pos, pos + writeLength));
+      written += writeLength;
+    }
+    controller.close();
+
+    return completer.future;
+  }
+
+  // Test parsing the data three times delivering the data in
+  // different chunks.
+  List<int> data = message.codeUnits;
+  expect(Future.wait([
+      testWrite(data),
+      testWrite(data, 10),
+      testWrite(data, 2),
+      testWrite(data, 1)]),
+      completes);
+}
+
+void testParseValid() {
+  String message;
+  Map headers;
+  Map headers1;
+  Map headers2;
+  Map headers3;
+  Map headers4;
+  String body1;
+  String body2;
+  String body3;
+  String body4;
+
+  // Sample from Wikipedia.
+  message = """
+This is a message with multiple parts in MIME format.\r
+--frontier\r
+Content-Type: text/plain\r
+\r
+This is the body of the message.\r
+--frontier\r
+Content-Type: application/octet-stream\r
+Content-Transfer-Encoding: base64\r
+\r
+PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg
+Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=\r
+--frontier--\r\n""";
+  headers1 = <String, String>{"content-type": "text/plain"};
+  headers2 = <String, String>{"content-type": "application/octet-stream",
+                              "content-transfer-encoding": "base64"};
+  body1 = "This is the body of the message.";
+  body2 = """
+PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg
+Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=""";
+  testParse(message, "frontier", [headers1, headers2], [body1, body2]);
+
+  // Sample from HTML 4.01 Specification.
+  message = """
+\r\n--AaB03x\r
+Content-Disposition: form-data; name=\"submit-name\"\r
+\r
+Larry\r
+--AaB03x\r
+Content-Disposition: form-data; name=\"files\"; filename=\"file1.txt\"\r
+Content-Type: text/plain\r
+\r
+... contents of file1.txt ...\r
+--AaB03x--\r\n""";
+  headers1 = <String, String>{
+      "content-disposition": "form-data; name=\"submit-name\""};
+  headers2 = <String, String>{
+      "content-type": "text/plain",
+      "content-disposition": "form-data; name=\"files\"; filename=\"file1.txt\""
+  };
+  body1 = "Larry";
+  body2 = "... contents of file1.txt ...";
+  testParse(message, "AaB03x", [headers1, headers2], [body1, body2]);
+
+  // Longer form from submitting the following from Chrome.
+  //
+  // <html>
+  // <body>
+  // <FORM action="http://127.0.0.1:1234/"
+  //     enctype="multipart/form-data"
+  //     method="post">
+  //  <P>
+  //  Text: <INPUT type="text" name="text_input">
+  //  Password: <INPUT type="password" name="password_input">
+  //  Checkbox: <INPUT type="checkbox" name="checkbox_input">
+  //  Radio: <INPUT type="radio" name="radio_input">
+  //  Send <INPUT type="submit">
+  //  </P>
+  // </FORM>
+  // </body>
+  // </html>
+
+  message = """
+\r\n------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r
+Content-Disposition: form-data; name=\"text_input\"\r
+\r
+text\r
+------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r
+Content-Disposition: form-data; name=\"password_input\"\r
+\r
+password\r
+------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r
+Content-Disposition: form-data; name=\"checkbox_input\"\r
+\r
+on\r
+------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r
+Content-Disposition: form-data; name=\"radio_input\"\r
+\r
+on\r
+------WebKitFormBoundaryQ3cgYAmGRF8yOeYB--\r\n""";
+  headers1 = <String, String>{
+      "content-disposition": "form-data; name=\"text_input\""};
+  headers2 = <String, String>{
+      "content-disposition": "form-data; name=\"password_input\""};
+  headers3 = <String, String>{
+      "content-disposition": "form-data; name=\"checkbox_input\""};
+  headers4 = <String, String>{
+      "content-disposition": "form-data; name=\"radio_input\""};
+  body1 = "text";
+  body2 = "password";
+  body3 = "on";
+  body4 = "on";
+  testParse(message,
+            "----WebKitFormBoundaryQ3cgYAmGRF8yOeYB",
+            [headers1, headers2, headers3, headers4],
+            [body1, body2, body3, body4]);
+
+  // Same form from Firefox.
+  message = """
+\r\n-----------------------------52284550912143824192005403738\r
+Content-Disposition: form-data; name=\"text_input\"\r
+\r
+text\r
+-----------------------------52284550912143824192005403738\r
+Content-Disposition: form-data; name=\"password_input\"\r
+\r
+password\r
+-----------------------------52284550912143824192005403738\r
+Content-Disposition: form-data; name=\"checkbox_input\"\r
+\r
+on\r
+-----------------------------52284550912143824192005403738\r
+Content-Disposition: form-data; name=\"radio_input\"\r
+\r
+on\r
+-----------------------------52284550912143824192005403738--\r\n""";
+  testParse(message,
+            "---------------------------52284550912143824192005403738",
+            [headers1, headers2, headers3, headers4],
+            [body1, body2, body3, body4]);
+
+  // And Internet Explorer
+  message = """
+\r\n-----------------------------7dc8f38c60326\r
+Content-Disposition: form-data; name=\"text_input\"\r
+\r
+text\r
+-----------------------------7dc8f38c60326\r
+Content-Disposition: form-data; name=\"password_input\"\r
+\r
+password\r
+-----------------------------7dc8f38c60326\r
+Content-Disposition: form-data; name=\"checkbox_input\"\r
+\r
+on\r
+-----------------------------7dc8f38c60326\r
+Content-Disposition: form-data; name=\"radio_input\"\r
+\r
+on\r
+-----------------------------7dc8f38c60326--\r\n""";
+  testParse(message,
+            "---------------------------7dc8f38c60326",
+            [headers1, headers2, headers3, headers4],
+            [body1, body2, body3, body4]);
+
+  // Test boundary prefix inside prefix and content.
+  message = """
+-\r
+--\r
+--b\r
+--bo\r
+--bou\r
+--boun\r
+--bound\r
+--bounda\r
+--boundar\r
+--boundary\r
+Content-Type: text/plain\r
+\r
+-\r
+--\r
+--b\r
+--bo\r
+--bou\r
+--boun\r
+--bound\r\r
+--bounda\r\r\r
+--boundar\r\r\r\r
+--boundary\r
+Content-Type: text/plain\r
+\r
+--boundar\r
+--bounda\r
+--bound\r
+--boun\r
+--bou\r
+--bo\r
+--b\r\r\r\r
+--\r\r\r
+-\r\r
+--boundary--\r\n""";
+  headers = <String, String>{"content-type": "text/plain"};
+  body1 = """
+-\r
+--\r
+--b\r
+--bo\r
+--bou\r
+--boun\r
+--bound\r\r
+--bounda\r\r\r
+--boundar\r\r\r""";
+  body2 = """
+--boundar\r
+--bounda\r
+--bound\r
+--boun\r
+--bou\r
+--bo\r
+--b\r\r\r\r
+--\r\r\r
+-\r""";
+  testParse(message, "boundary", [headers, headers], [body1, body2]);
+
+  // Without initial CRLF.
+  message = """
+--xxx\r
+\r
+\r
+Body 1\r
+--xxx\r
+\r
+\r
+Body2\r
+--xxx--\r\n""";
+  testParse(message, "xxx", null, ["\r\nBody 1", "\r\nBody2"]);
+}
+
+void testParseInvalid() {
+  String message;
+
+  // Missing end boundary.
+  message = """
+\r
+--xxx\r
+\r
+\r
+Body 1\r
+--xxx\r
+\r
+\r
+Body2\r
+--xxx\r\n""";
+  testParse(message, "xxx", null, [null, null], true);
+}
+
+void main() {
+  testParseValid();
+  testParseInvalid();
+}