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();
+}