blob: 73bc4956049dfb4a989054df9aabbae4a83ad56b [file] [log] [blame]
// 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';
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) {
return byte > 31 && byte < 128 && !_SEPARATORS.contains(byte);
}
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 MimeMultipartException('Failed to parse multipart mime 1');
}
}
void _expectWhitespace(int byte) {
if (byte != CharCode.SP && byte != CharCode.HT) {
throw 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}) {
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;
final _controller = StreamController<MimeMultipart>(sync: true);
Stream<MimeMultipart> get stream => _controller.stream;
late StreamSubscription _subscription;
StreamController<List<int>>? _multipartController;
Map<String, String>? _headers;
int _state = _START;
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 = _CONTROLLER_STATE_CANCELED;
_tryPropagateControllerState();
}
..onListen = () {
_controllerState = _CONTROLLER_STATE_ACTIVE;
_subscription = stream.listen((data) {
assert(_buffer == _placeholderBuffer);
_subscription.pause();
_buffer = data;
_index = 0;
_parse();
}, onDone: () {
if (_state != _DONE) {
_controller
.addError(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 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.
var 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 == _CONTENT && _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) {
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));
}
}
while (_index < _buffer.length && _state != _FAIL && _state != _DONE) {
var byte =
_index < 0 ? _boundary[boundaryPrefix + _index] : _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);
_multipartController?.close();
if (_multipartController != null) {
_multipartController = null;
_tryPropagateControllerState();
}
_state = _HEADER_START;
break;
case _HEADER_START:
_headers = <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 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 {
var headerField = utf8.decode(_headerField);
var 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 = StreamController(
sync: true,
onListen: () {
if (_subscription.isPaused) _subscription.resume();
},
onPause: _subscription.pause,
onResume: _subscription.resume);
_controller
.add(_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;
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();
if (_multipartController != null) {
_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 = _placeholderBuffer;
_index = 0;
_subscription.resume();
}
}
}
// Used as a placeholder instead of having a nullable buffer.
const _placeholderBuffer = <int>[];