blob: 109a9bcd8fd97f69e2c491b43553097752fda1c6 [file] [log] [blame]
// 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 'dart:async';
import 'dart:math';
import 'package:mime/mime.dart';
import 'package:test/test.dart';
void _writeInChunks(
List<int> data, int chunkSize, StreamController<List<int>> controller) {
if (chunkSize == -1) chunkSize = data.length;
for (var pos = 0; pos < data.length; pos += chunkSize) {
final remaining = data.length - pos;
final writeLength = min(chunkSize, remaining);
controller.add(data.sublist(pos, pos + writeLength));
}
controller.close();
}
enum TestMode { immediateListen, delayListen, pauseResume }
void _runParseTest(
String message,
String boundary,
TestMode mode, [
List<Map<String, String>>? expectedHeaders,
List<String?>? expectedParts,
bool expectError = false,
]) {
Future<void> testWrite(List<int> data, [int chunkSize = -1]) {
final controller = StreamController<List<int>>(sync: true);
final stream =
controller.stream.transform(MimeMultipartTransformer(boundary));
var i = 0;
final completer = Completer<void>();
final futures = <Future<void>>[];
stream.listen((multipart) {
final part = i++;
if (expectedHeaders != null) {
expect(multipart.headers, equals(expectedHeaders[part]));
}
switch (mode) {
case TestMode.immediateListen:
futures.add(multipart.fold<List<int>>(
[], (buffer, data) => buffer..addAll(data)).then((data) {
if (expectedParts?[part] != null) {
expect(data, equals(expectedParts?[part]!.codeUnits));
}
}));
case TestMode.delayListen:
futures.add(
Future(
() => multipart.fold<List<int>>(
[],
(buffer, data) => buffer..addAll(data),
).then(
(data) {
if (expectedParts?[part] != null) {
expect(data, equals(expectedParts?[part]!.codeUnits));
}
},
),
),
);
case TestMode.pauseResume:
final completer = Completer<void>();
futures.add(completer.future);
final buffer = <int>[];
late StreamSubscription<List<int>> subscription;
subscription = multipart.listen((data) {
buffer.addAll(data);
subscription.pause();
Future(() => subscription.resume());
}, onDone: () {
if (expectedParts?[part] != null) {
expect(buffer, equals(expectedParts?[part]!.codeUnits));
}
completer.complete();
});
addTearDown(subscription.cancel);
}
}, onError: (Object error) {
// ignore: only_throw_errors
if (!expectError) throw error;
}, onDone: () {
if (expectedParts != null) {
expect(i, equals(expectedParts.length));
}
Future.wait(futures).then(completer.complete);
});
_writeInChunks(data, chunkSize, controller);
return completer.future;
}
Future<void> testFirstPartOnly(List<int> data, [int chunkSize = -1]) {
final completer = Completer<void>();
final controller = StreamController<List<int>>(sync: true);
final stream =
controller.stream.transform(MimeMultipartTransformer(boundary));
stream.first.then((multipart) {
if (expectedHeaders != null) {
expect(multipart.headers, equals(expectedHeaders[0]));
}
return multipart.fold<List<int>>([], (b, d) => b..addAll(d)).then(
(data) {
if (expectedParts != null && expectedParts[0] != null) {
expect(data, equals(expectedParts[0]!.codeUnits));
}
},
);
}).then((_) {
completer.complete();
});
_writeInChunks(data, chunkSize, controller);
return completer.future;
}
Future<void> testCompletePartAfterCancel(List<int> data, int parts,
[int chunkSize = -1]) {
final completer = Completer<void>();
final controller = StreamController<List<int>>(sync: true);
final stream =
controller.stream.transform(MimeMultipartTransformer(boundary));
late StreamSubscription<void> subscription;
var i = 0;
final futures = <Future<void>>[];
subscription = stream.listen((multipart) {
final partIndex = i;
if (partIndex >= parts) {
throw StateError('Expected no more parts, but got one.');
}
if (expectedHeaders != null) {
expect(multipart.headers, equals(expectedHeaders[partIndex]));
}
futures.add(
multipart.fold<List<int>>([], (b, d) => b..addAll(d)).then((data) {
if (expectedParts != null && expectedParts[partIndex] != null) {
expect(data, equals(expectedParts[partIndex]!.codeUnits));
}
}));
if (partIndex == (parts - 1)) {
subscription.cancel();
Future.wait(futures).then(completer.complete);
}
i++;
});
_writeInChunks(data, chunkSize, controller);
return completer.future;
}
// Test parsing the data three times delivering the data in
// different chunks.
final data = message.codeUnits;
test('test', () {
expect(
Future.wait([
testWrite(data),
testWrite(data, 10),
testWrite(data, 2),
testWrite(data, 1),
]),
completes);
});
if (expectedParts!.isNotEmpty) {
test('test-first-part-only', () {
expect(
Future.wait([
testFirstPartOnly(data),
testFirstPartOnly(data, 10),
testFirstPartOnly(data, 2),
testFirstPartOnly(data, 1),
]),
completes);
});
test('test-n-parts-only', () {
var numPartsExpected = expectedParts.length - 1;
if (numPartsExpected == 0) numPartsExpected = 1;
expect(
Future.wait([
testCompletePartAfterCancel(data, numPartsExpected),
testCompletePartAfterCancel(data, numPartsExpected, 10),
testCompletePartAfterCancel(data, numPartsExpected, 2),
testCompletePartAfterCancel(data, numPartsExpected, 1),
]),
completes);
});
}
}
void _testParse(String message, String boundary,
[List<Map<String, String>>? expectedHeaders,
List<String?>? expectedParts,
bool expectError = false]) {
_runParseTest(message, boundary, TestMode.immediateListen, expectedHeaders,
expectedParts, expectError);
_runParseTest(message, boundary, TestMode.delayListen, expectedHeaders,
expectedParts, expectError);
_runParseTest(message, boundary, TestMode.pauseResume, expectedHeaders,
expectedParts, expectError);
}
void _testParseValid() {
// Empty message from Chrome form post.
var message = '------WebKitFormBoundaryU3FBruSkJKG0Yor1--\r\n';
_testParse(message, '----WebKitFormBoundaryU3FBruSkJKG0Yor1', [], []);
// 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''';
var headers1 = <String, String>{'content-type': 'text/plain'};
var headers2 = <String, String>{
'content-type': 'application/octet-stream',
'content-transfer-encoding': 'base64'
};
var body1 = 'This is the body of the message.';
var 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"'
};
final headers3 = <String, String>{
'content-disposition': 'form-data; name="checkbox_input"'
};
final headers4 = <String, String>{
'content-disposition': 'form-data; name="radio_input"'
};
body1 = 'text';
body2 = 'password';
const body3 = 'on';
const 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''';
final 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() {
// Missing end boundary.
const 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();
}