| // 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 'char_code.dart'; |
| import 'mime_shared.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 _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 = []; |
| |
| // The following states belong to `_controller`, state changes will not be |
| // immediately acted upon but rather only after the current |
| // `_multipartController` is done. |
| static const int _CONTROLLER_STATE_IDLE = 0; |
| static const int _CONTROLLER_STATE_ACTIVE = 1; |
| static const int _CONTROLLER_STATE_PAUSED = 2; |
| static const int _CONTROLLER_STATE_CANCELED = 3; |
| |
| int _controllerState = _CONTROLLER_STATE_IDLE; |
| |
| StreamController<MimeMultipart> _controller; |
| |
| Stream<MimeMultipart> get stream => _controller.stream; |
| |
| StreamSubscription _subscription; |
| |
| StreamController<List<int>> _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: () { |
| _controllerState = _CONTROLLER_STATE_CANCELED; |
| _tryPropagateControllerState(); |
| }, |
| onListen: () { |
| _controllerState = _CONTROLLER_STATE_ACTIVE; |
| _subscription = stream.listen((data) { |
| assert(_buffer == null); |
| _subscription.pause(); |
| _buffer = data; |
| _index = 0; |
| _parse(); |
| }, onDone: () { |
| if (_state != _DONE) { |
| _controller |
| .addError(new MimeMultipartException("Bad multipart ending")); |
| } |
| _controller.close(); |
| }, onError: _controller.addError); |
| }); |
| } |
| |
| void _resumeStream() { |
| assert(_controllerState == _CONTROLLER_STATE_PAUSED); |
| _controllerState = _CONTROLLER_STATE_ACTIVE; |
| _tryPropagateControllerState(); |
| } |
| |
| void _pauseStream() { |
| _controllerState = _CONTROLLER_STATE_PAUSED; |
| _tryPropagateControllerState(); |
| } |
| |
| void _tryPropagateControllerState() { |
| if (_multipartController == null) { |
| switch (_controllerState) { |
| case _CONTROLLER_STATE_ACTIVE: |
| if (_subscription.isPaused) _subscription.resume(); |
| break; |
| case _CONTROLLER_STATE_PAUSED: |
| if (!_subscription.isPaused) _subscription.pause(); |
| break; |
| case _CONTROLLER_STATE_CANCELED: |
| _subscription.cancel(); |
| break; |
| default: |
| throw new StateError("This code should never be reached."); |
| } |
| } |
| } |
| |
| 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) { |
| 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 = _BOUNDARY_ENDING; |
| _boundaryIndex = 0; |
| } |
| } else { |
| // Restart matching of the boundary. |
| _index = _index - _boundaryIndex; |
| _boundaryIndex = 0; |
| } |
| 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); |
| if (_multipartController != null) { |
| _multipartController.close(); |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| } |
| _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, |
| onListen: () { |
| if (_subscription.isPaused) _subscription.resume(); |
| }, |
| onPause: _subscription.pause, |
| onResume: _subscription.resume); |
| _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(); |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| _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); |
| if (_multipartController != null) { |
| _multipartController.close(); |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| } |
| _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; |
| _subscription.resume(); |
| } |
| } |
| } |