blob: e843727314bb3472dc1ee77a20e0e5b64d5bea71 [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.
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();
}
}
}