| // 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. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| |
| import 'char_code.dart' as char_code; |
| import 'mime_shared.dart'; |
| |
| /// Bytes for '()<>@,;:\\"/[]?={} \t'. |
| const _separators = { |
| 40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, 91, 93, 63, 61, 123, 125, 32, 9 // |
| }; |
| |
| bool _isTokenChar(int byte) => |
| byte > 31 && byte < 128 && !_separators.contains(byte); |
| |
| int _toLowerCase(int byte) { |
| const delta = char_code.lowerA - char_code.upperA; |
| return (char_code.upperA <= byte && byte <= char_code.upperZ) |
| ? byte + delta |
| : byte; |
| } |
| |
| void _expectByteValue(int val1, int val2) { |
| if (val1 != val2) { |
| throw const MimeMultipartException('Failed to parse multipart mime 1'); |
| } |
| } |
| |
| void _expectWhitespace(int byte) { |
| if (byte != char_code.sp && byte != char_code.ht) { |
| throw const MimeMultipartException('Failed to parse multipart mime 2'); |
| } |
| } |
| |
| class _MimeMultipart extends MimeMultipart { |
| @override |
| final Map<String, String> headers; |
| final Stream<List<int>> _stream; |
| |
| _MimeMultipart(this.headers, this._stream); |
| |
| @override |
| StreamSubscription<List<int>> listen( |
| void Function(List<int> data)? onData, { |
| void Function()? onDone, |
| Function? onError, |
| bool? cancelOnError, |
| }) => |
| _stream.listen( |
| onData, |
| onDone: onDone, |
| onError: onError, |
| cancelOnError: cancelOnError, |
| ); |
| } |
| |
| class BoundMultipartStream { |
| static const int _startCode = 0; |
| static const int _boundaryEndingCode = 1; |
| static const int _boundaryEndCode = 2; |
| static const int _headerStartCode = 3; |
| static const int _headerFieldCode = 4; |
| static const int _headerValueStartCode = 5; |
| static const int _headerValueCode = 6; |
| static const int _headerValueFoldingOrEndingCode = 7; |
| static const int _headerValueFoldOrEndCode = 8; |
| static const int _headerEndingCode = 9; |
| static const int _contentCode = 10; |
| static const int _lastBoundaryDash2Code = 11; |
| static const int _lastBoundaryEndingCode = 12; |
| static const int _lastBoundaryEndCode = 13; |
| static const int _doneCode = 14; |
| static const int _failCode = 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 _controllerStateIdle = 0; |
| static const int _controllerStateActive = 1; |
| static const int _controllerStatePaused = 2; |
| static const int _controllerStateCanceled = 3; |
| |
| int _controllerState = _controllerStateIdle; |
| |
| final _controller = StreamController<MimeMultipart>(sync: true); |
| |
| Stream<MimeMultipart> get stream => _controller.stream; |
| |
| late StreamSubscription<void> _subscription; |
| |
| StreamController<List<int>>? _multipartController; |
| Map<String, String>? _headers; |
| |
| int _state = _startCode; |
| int _boundaryIndex = 2; |
| |
| /// Current index into [_buffer]. |
| /// |
| /// If index is negative then it is the index into the artificial prefix of |
| /// the boundary string. |
| int _index = 0; |
| List<int> _buffer = _placeholderBuffer; |
| |
| BoundMultipartStream(this._boundary, Stream<List<int>> stream) { |
| _controller |
| ..onPause = _pauseStream |
| ..onResume = _resumeStream |
| ..onCancel = () { |
| _controllerState = _controllerStateCanceled; |
| _tryPropagateControllerState(); |
| } |
| ..onListen = () { |
| _controllerState = _controllerStateActive; |
| _subscription = stream.listen((data) { |
| assert(_buffer == _placeholderBuffer); |
| _subscription.pause(); |
| _buffer = data; |
| _index = 0; |
| _parse(); |
| }, onDone: () { |
| if (_state != _doneCode) { |
| _controller |
| .addError(const MimeMultipartException('Bad multipart ending')); |
| } |
| _controller.close(); |
| }, onError: _controller.addError); |
| }; |
| } |
| |
| void _resumeStream() { |
| assert(_controllerState == _controllerStatePaused); |
| _controllerState = _controllerStateActive; |
| _tryPropagateControllerState(); |
| } |
| |
| void _pauseStream() { |
| _controllerState = _controllerStatePaused; |
| _tryPropagateControllerState(); |
| } |
| |
| void _tryPropagateControllerState() { |
| if (_multipartController == null) { |
| switch (_controllerState) { |
| case _controllerStateActive: |
| if (_subscription.isPaused) _subscription.resume(); |
| case _controllerStatePaused: |
| if (!_subscription.isPaused) _subscription.pause(); |
| case _controllerStateCanceled: |
| _subscription.cancel(); |
| default: |
| throw StateError('This code should never be reached.'); |
| } |
| } |
| } |
| |
| void _parse() { |
| // Number of boundary bytes to artificially place before the supplied data. |
| // The data to parse might be 'artificially' prefixed with a |
| // partial match of the boundary. |
| final boundaryPrefix = _boundaryIndex; |
| // 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. |
| var contentStartIndex = |
| _state == _contentCode && _boundaryIndex == 0 ? 0 : null; |
| |
| // 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) { |
| final 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 { |
| final contentEndIndex = _index - _boundaryIndex; |
| _multipartController! |
| .add(_buffer.sublist(contentStartIndex, contentEndIndex)); |
| } |
| } |
| |
| while ( |
| _index < _buffer.length && _state != _failCode && _state != _doneCode) { |
| final byte = |
| _index < 0 ? _boundary[boundaryPrefix + _index] : _buffer[_index]; |
| switch (_state) { |
| case _startCode: |
| if (byte == _boundary[_boundaryIndex]) { |
| _boundaryIndex++; |
| if (_boundaryIndex == _boundary.length) { |
| _state = _boundaryEndingCode; |
| _boundaryIndex = 0; |
| } |
| } else { |
| // Restart matching of the boundary. |
| _index = _index - _boundaryIndex; |
| _boundaryIndex = 0; |
| } |
| |
| case _boundaryEndingCode: |
| if (byte == char_code.cr) { |
| _state = _boundaryEndCode; |
| } else if (byte == char_code.dash) { |
| _state = _lastBoundaryDash2Code; |
| } else { |
| _expectWhitespace(byte); |
| } |
| |
| case _boundaryEndCode: |
| _expectByteValue(byte, char_code.lf); |
| _multipartController?.close(); |
| if (_multipartController != null) { |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| } |
| _state = _headerStartCode; |
| |
| case _headerStartCode: |
| _headers = <String, String>{}; |
| if (byte == char_code.cr) { |
| _state = _headerEndingCode; |
| } else { |
| // Start of new header field. |
| _headerField.add(_toLowerCase(byte)); |
| _state = _headerFieldCode; |
| } |
| |
| case _headerFieldCode: |
| if (byte == char_code.colon) { |
| _state = _headerValueStartCode; |
| } else { |
| if (!_isTokenChar(byte)) { |
| throw const MimeMultipartException('Invalid header field name'); |
| } |
| _headerField.add(_toLowerCase(byte)); |
| } |
| |
| case _headerValueStartCode: |
| if (byte == char_code.cr) { |
| _state = _headerValueFoldingOrEndingCode; |
| } else if (byte != char_code.sp && byte != char_code.ht) { |
| // Start of new header value. |
| _headerValue.add(byte); |
| _state = _headerValueCode; |
| } |
| |
| case _headerValueCode: |
| if (byte == char_code.cr) { |
| _state = _headerValueFoldingOrEndingCode; |
| } else { |
| _headerValue.add(byte); |
| } |
| |
| case _headerValueFoldingOrEndingCode: |
| _expectByteValue(byte, char_code.lf); |
| _state = _headerValueFoldOrEndCode; |
| |
| case _headerValueFoldOrEndCode: |
| if (byte == char_code.sp || byte == char_code.ht) { |
| _state = _headerValueStartCode; |
| } else { |
| final headerField = utf8.decode(_headerField); |
| final headerValue = utf8.decode(_headerValue); |
| _headers![headerField.toLowerCase()] = headerValue; |
| _headerField.clear(); |
| _headerValue.clear(); |
| if (byte == char_code.cr) { |
| _state = _headerEndingCode; |
| } else { |
| // Start of new header field. |
| _headerField.add(_toLowerCase(byte)); |
| _state = _headerFieldCode; |
| } |
| } |
| |
| case _headerEndingCode: |
| _expectByteValue(byte, char_code.lf); |
| _multipartController = StreamController( |
| sync: true, |
| onListen: () { |
| if (_subscription.isPaused) _subscription.resume(); |
| }, |
| onPause: _subscription.pause, |
| onResume: _subscription.resume); |
| _controller |
| .add(_MimeMultipart(_headers!, _multipartController!.stream)); |
| _headers = null; |
| _state = _contentCode; |
| contentStartIndex = _index + 1; |
| |
| case _contentCode: |
| if (byte == _boundary[_boundaryIndex]) { |
| _boundaryIndex++; |
| if (_boundaryIndex == _boundary.length) { |
| if (contentStartIndex != null) { |
| _index++; |
| reportData(); |
| _index--; |
| } |
| _multipartController!.close(); |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| _boundaryIndex = 0; |
| _state = _boundaryEndingCode; |
| } |
| } else { |
| // Restart matching of the boundary. |
| _index = _index - _boundaryIndex; |
| contentStartIndex ??= _index; |
| _boundaryIndex = 0; |
| } |
| |
| case _lastBoundaryDash2Code: |
| _expectByteValue(byte, char_code.dash); |
| _state = _lastBoundaryEndingCode; |
| |
| case _lastBoundaryEndingCode: |
| if (byte == char_code.cr) { |
| _state = _lastBoundaryEndCode; |
| } else { |
| _expectWhitespace(byte); |
| } |
| |
| case _lastBoundaryEndCode: |
| _expectByteValue(byte, char_code.lf); |
| _multipartController?.close(); |
| if (_multipartController != null) { |
| _multipartController = null; |
| _tryPropagateControllerState(); |
| } |
| _state = _doneCode; |
| |
| default: |
| // Should be unreachable. |
| assert(false); |
| break; |
| } |
| |
| // Move to the next byte. |
| _index++; |
| } |
| |
| // Report any known content. |
| if (_state == _contentCode && contentStartIndex != null) { |
| reportData(); |
| } |
| |
| // Resume if at end. |
| if (_index == _buffer.length) { |
| _buffer = _placeholderBuffer; |
| _index = 0; |
| _subscription.resume(); |
| } |
| } |
| } |
| |
| // Used as a placeholder instead of having a nullable buffer. |
| const _placeholderBuffer = <int>[]; |