| // Copyright (c) 2016, 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 'dart:math'; | |
| import 'package:http_parser/http_parser.dart'; | |
| import 'package:http_parser/src/chunked_coding/charcodes.dart'; | |
| import 'package:test/test.dart'; | |
| void main() { | |
| group('encoder', () { | |
| test('adds a header to the chunk of bytes', () { | |
| expect(chunkedCoding.encode([1, 2, 3]), | |
| equals([$3, $cr, $lf, 1, 2, 3, $cr, $lf, $0, $cr, $lf, $cr, $lf])); | |
| }); | |
| test('uses hex for chunk size', () { | |
| final data = Iterable<int>.generate(0xA7).toList(); | |
| expect( | |
| chunkedCoding.encode(data), | |
| equals( | |
| [$a, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf])); | |
| }); | |
| test('just generates a footer for an empty input', () { | |
| expect(chunkedCoding.encode([]), equals([$0, $cr, $lf, $cr, $lf])); | |
| }); | |
| group('with chunked conversion', () { | |
| late List<List<int>> results; | |
| late ByteConversionSink sink; | |
| setUp(() { | |
| results = []; | |
| final controller = StreamController<List<int>>(sync: true); | |
| controller.stream.listen(results.add); | |
| sink = chunkedCoding.encoder.startChunkedConversion(controller.sink); | |
| }); | |
| test('adds headers to each chunk of bytes', () { | |
| sink.add([1, 2, 3, 4]); | |
| expect( | |
| results, | |
| equals([ | |
| [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf] | |
| ])); | |
| sink.add([5, 6, 7]); | |
| expect( | |
| results, | |
| equals([ | |
| [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf], | |
| [$3, $cr, $lf, 5, 6, 7, $cr, $lf], | |
| ])); | |
| sink.close(); | |
| expect( | |
| results, | |
| equals([ | |
| [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf], | |
| [$3, $cr, $lf, 5, 6, 7, $cr, $lf], | |
| [$0, $cr, $lf, $cr, $lf], | |
| ])); | |
| }); | |
| test('handles empty chunks', () { | |
| sink.add([]); | |
| expect(results, equals([<int>[]])); | |
| sink.add([1, 2, 3]); | |
| expect( | |
| results, | |
| equals([ | |
| <int>[], | |
| [$3, $cr, $lf, 1, 2, 3, $cr, $lf] | |
| ])); | |
| sink.add([]); | |
| expect( | |
| results, | |
| equals([ | |
| <int>[], | |
| [$3, $cr, $lf, 1, 2, 3, $cr, $lf], | |
| <int>[] | |
| ])); | |
| sink.close(); | |
| expect( | |
| results, | |
| equals([ | |
| <int>[], | |
| [$3, $cr, $lf, 1, 2, 3, $cr, $lf], | |
| <int>[], | |
| [$0, $cr, $lf, $cr, $lf], | |
| ])); | |
| }); | |
| group('addSlice()', () { | |
| test('adds bytes from the specified slice', () { | |
| sink.addSlice([1, 2, 3, 4, 5], 1, 4, false); | |
| expect( | |
| results, | |
| equals([ | |
| [$3, $cr, $lf, 2, 3, 4, $cr, $lf] | |
| ])); | |
| }); | |
| test("doesn't add a header if the slice is empty", () { | |
| sink.addSlice([1, 2, 3, 4, 5], 1, 1, false); | |
| expect(results, equals([<int>[]])); | |
| }); | |
| test('adds a footer if isLast is true', () { | |
| sink.addSlice([1, 2, 3, 4, 5], 1, 4, true); | |
| expect( | |
| results, | |
| equals([ | |
| [$3, $cr, $lf, 2, 3, 4, $cr, $lf, $0, $cr, $lf, $cr, $lf] | |
| ])); | |
| // Setting isLast shuld close the sink. | |
| expect(() => sink.add([]), throwsStateError); | |
| }); | |
| group('disallows', () { | |
| test('start < 0', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false), | |
| throwsRangeError); | |
| }); | |
| test('start > end', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false), | |
| throwsRangeError); | |
| }); | |
| test('end > length', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false), | |
| throwsRangeError); | |
| }); | |
| }); | |
| }); | |
| }); | |
| }); | |
| group('decoder', () { | |
| test('parses chunked data', () { | |
| expect( | |
| chunkedCoding.decode([ | |
| $3, | |
| $cr, | |
| $lf, | |
| 1, | |
| 2, | |
| 3, | |
| $cr, | |
| $lf, | |
| $4, | |
| $cr, | |
| $lf, | |
| 4, | |
| 5, | |
| 6, | |
| 7, | |
| $cr, | |
| $lf, | |
| $0, | |
| $cr, | |
| $lf, | |
| $cr, | |
| $lf, | |
| ]), | |
| equals([1, 2, 3, 4, 5, 6, 7])); | |
| }); | |
| test('parses hex size', () { | |
| final data = Iterable<int>.generate(0xA7).toList(); | |
| expect( | |
| chunkedCoding.decode( | |
| [$a, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf]), | |
| equals(data)); | |
| }); | |
| test('parses capital hex size', () { | |
| final data = Iterable<int>.generate(0xA7).toList(); | |
| expect( | |
| chunkedCoding.decode( | |
| [$A, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf]), | |
| equals(data)); | |
| }); | |
| test('parses an empty message', () { | |
| expect(chunkedCoding.decode([$0, $cr, $lf, $cr, $lf]), isEmpty); | |
| }); | |
| test('parses hex size with leading zeros', () { | |
| expect( | |
| chunkedCoding | |
| .decode(ascii.encode('00000000000000000003\r\nabc\r\n0\r\n\r\n')), | |
| equals(ascii.encode('abc'))); | |
| }); | |
| group('disallows a message', () { | |
| test('that ends without any input', () { | |
| expect(() => chunkedCoding.decode([]), throwsFormatException); | |
| }); | |
| test('that ends after the size', () { | |
| expect(() => chunkedCoding.decode([$a]), throwsFormatException); | |
| }); | |
| test('that ends after CR', () { | |
| expect(() => chunkedCoding.decode([$a, $cr]), throwsFormatException); | |
| }); | |
| test('that ends after LF', () { | |
| expect( | |
| () => chunkedCoding.decode([$a, $cr, $lf]), throwsFormatException); | |
| }); | |
| test('that ends after insufficient bytes', () { | |
| expect(() => chunkedCoding.decode([$a, $cr, $lf, 1, 2, 3]), | |
| throwsFormatException); | |
| }); | |
| test('that ends after insufficient bytes for max chunk size', () { | |
| expect(() => chunkedCoding.decode(ascii.encode('800000000000\r\na')), | |
| throwsFormatException); | |
| }); | |
| test("that ends after a chunk's bytes", () { | |
| expect(() => chunkedCoding.decode([$1, $cr, $lf, 1]), | |
| throwsFormatException); | |
| }); | |
| test("that ends after a chunk's CR", () { | |
| expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr]), | |
| throwsFormatException); | |
| }); | |
| test("that ends atfter a chunk's LF", () { | |
| expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr, $lf]), | |
| throwsFormatException); | |
| }); | |
| test('that ends after the empty chunk', () { | |
| expect( | |
| () => chunkedCoding.decode([$0, $cr, $lf]), throwsFormatException); | |
| }); | |
| test('that ends after the closing CR', () { | |
| expect(() => chunkedCoding.decode([$0, $cr, $lf, $cr]), | |
| throwsFormatException); | |
| }); | |
| test('with a chunk without a size', () { | |
| expect(() => chunkedCoding.decode([$cr, $lf, $0, $cr, $lf, $cr, $lf]), | |
| throwsFormatException); | |
| }); | |
| test('with a chunk with a non-hex size', () { | |
| expect( | |
| () => chunkedCoding.decode([$q, $cr, $lf, $0, $cr, $lf, $cr, $lf]), | |
| throwsFormatException); | |
| expect( | |
| () => chunkedCoding.decode([-1, $cr, $lf, $0, $cr, $lf, $cr, $lf]), | |
| throwsFormatException); | |
| }); | |
| test('with a chunk size exceeding 128 TiB', () { | |
| expect(() => chunkedCoding.decode(ascii.encode('800000000001\r\na')), | |
| throwsFormatException); | |
| expect( | |
| () => chunkedCoding.decode(ascii.encode('8000000000000000\r\na')), | |
| throwsFormatException); | |
| expect( | |
| () => chunkedCoding.decode(ascii.encode('ffffffffffffffff\r\na')), | |
| throwsFormatException); | |
| }); | |
| test('with a chunk size exceeding 16 hex digits', () { | |
| expect( | |
| () => | |
| chunkedCoding.decode(ascii.encode('10000000000000000\r\n\r\n')), | |
| throwsFormatException); | |
| expect( | |
| () => chunkedCoding | |
| .decode(ascii.encode('10000000000000003\r\nabc\r\n0\r\n\r\n')), | |
| throwsFormatException); | |
| }); | |
| }); | |
| group('with chunked conversion', () { | |
| late List<List<int>> results; | |
| late ByteConversionSink sink; | |
| setUp(() { | |
| results = []; | |
| final controller = StreamController<List<int>>(sync: true); | |
| controller.stream.listen(results.add); | |
| sink = chunkedCoding.decoder.startChunkedConversion(controller.sink); | |
| }); | |
| test('decodes each chunk of bytes', () { | |
| sink.add([$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3, 4] | |
| ])); | |
| sink.add([$3, $cr, $lf, 5, 6, 7, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3, 4], | |
| [5, 6, 7] | |
| ])); | |
| sink.add([$0, $cr, $lf, $cr, $lf]); | |
| sink.close(); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3, 4], | |
| [5, 6, 7] | |
| ])); | |
| }); | |
| test('handles empty chunks', () { | |
| sink.add([]); | |
| expect(results, isEmpty); | |
| sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| sink.add([]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| sink.add([$0, $cr, $lf, $cr, $lf]); | |
| sink.close(); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| }); | |
| test('throws if the sink is closed before the message is done', () { | |
| sink.add([$3, $cr, $lf, 1, 2, 3]); | |
| expect(() => sink.close(), throwsFormatException); | |
| }); | |
| group('preserves state when a byte array ends', () { | |
| test('within chunk size', () { | |
| sink.add([$a]); | |
| expect(results, isEmpty); | |
| final data = Iterable<int>.generate(0xA7).toList(); | |
| sink.add([$7, $cr, $lf, ...data]); | |
| expect(results, equals([data])); | |
| }); | |
| test('after chunk size', () { | |
| sink.add([$3]); | |
| expect(results, isEmpty); | |
| sink.add([$cr, $lf, 1, 2, 3]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| }); | |
| test('after CR', () { | |
| sink.add([$3, $cr]); | |
| expect(results, isEmpty); | |
| sink.add([$lf, 1, 2, 3]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| }); | |
| test('after LF', () { | |
| sink.add([$3, $cr, $lf]); | |
| expect(results, isEmpty); | |
| sink.add([1, 2, 3]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| }); | |
| test('after some bytes', () { | |
| sink.add([$3, $cr, $lf, 1, 2]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2] | |
| ])); | |
| sink.add([3]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2], | |
| [3] | |
| ])); | |
| }); | |
| test('after all bytes', () { | |
| sink.add([$3, $cr, $lf, 1, 2, 3]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| sink.add([$cr, $lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3], | |
| [2, 3, 4] | |
| ])); | |
| }); | |
| test('after a post-chunk CR', () { | |
| sink.add([$3, $cr, $lf, 1, 2, 3, $cr]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| sink.add([$lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3], | |
| [2, 3, 4] | |
| ])); | |
| }); | |
| test('after a post-chunk LF', () { | |
| sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3] | |
| ])); | |
| sink.add([$3, $cr, $lf, 2, 3, 4, $cr, $lf]); | |
| expect( | |
| results, | |
| equals([ | |
| [1, 2, 3], | |
| [2, 3, 4] | |
| ])); | |
| }); | |
| test('after empty chunk size', () { | |
| sink.add([$0]); | |
| expect(results, isEmpty); | |
| sink.add([$cr, $lf, $cr, $lf]); | |
| expect(results, isEmpty); | |
| sink.close(); | |
| expect(results, isEmpty); | |
| }); | |
| test('after first empty chunk CR', () { | |
| sink.add([$0, $cr]); | |
| expect(results, isEmpty); | |
| sink.add([$lf, $cr, $lf]); | |
| expect(results, isEmpty); | |
| sink.close(); | |
| expect(results, isEmpty); | |
| }); | |
| test('after first empty chunk LF', () { | |
| sink.add([$0, $cr, $lf]); | |
| expect(results, isEmpty); | |
| sink.add([$cr, $lf]); | |
| expect(results, isEmpty); | |
| sink.close(); | |
| expect(results, isEmpty); | |
| }); | |
| test('after second empty chunk CR', () { | |
| sink.add([$0, $cr, $lf, $cr]); | |
| expect(results, isEmpty); | |
| sink.add([$lf]); | |
| expect(results, isEmpty); | |
| sink.close(); | |
| expect(results, isEmpty); | |
| }); | |
| }); | |
| group('addSlice()', () { | |
| test('adds bytes from the specified slice', () { | |
| sink.addSlice([1, $3, $cr, $lf, 2, 3, 4, 5], 1, 7, false); | |
| expect( | |
| results, | |
| equals([ | |
| [2, 3, 4] | |
| ])); | |
| }); | |
| test("doesn't decode if the slice is empty", () { | |
| sink.addSlice([1, 2, 3, 4, 5], 1, 1, false); | |
| expect(results, isEmpty); | |
| }); | |
| test('closes the sink if isLast is true', () { | |
| sink.addSlice([1, $0, $cr, $lf, $cr, $lf, 7], 1, 6, true); | |
| expect(results, isEmpty); | |
| }); | |
| group('disallows', () { | |
| test('start < 0', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false), | |
| throwsRangeError); | |
| }); | |
| test('start > end', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false), | |
| throwsRangeError); | |
| }); | |
| test('end > length', () { | |
| expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false), | |
| throwsRangeError); | |
| }); | |
| }); | |
| }); | |
| test('rejects chunk sizes exceeding 128 TiB', () { | |
| expect(() => sink.add(ascii.encode('800000000001\r\na')), | |
| throwsFormatException); | |
| expect(() => sink.add(ascii.encode('8000000000000000\r\na')), | |
| throwsFormatException); | |
| }); | |
| test('rejects chunk sizes exceeding 128 TiB across chunks', () { | |
| sink.add(ascii.encode('800000')); | |
| expect( | |
| () => sink.add(ascii.encode('000001\r\na')), throwsFormatException); | |
| }); | |
| test('rejects chunk sizes exceeding 16 hex digits across chunks', () { | |
| sink.add(ascii.encode('10000000')); | |
| expect(() => sink.add(ascii.encode('000000000\r\n\r\n')), | |
| throwsFormatException); | |
| }); | |
| test('handles max chunk size without start + size overflow', () { | |
| sink.add(ascii.encode('800000000000\r\na')); | |
| expect( | |
| results, | |
| equals([ | |
| [$a] | |
| ])); | |
| expect(() => sink.close(), throwsFormatException); | |
| }); | |
| test('matches single-chunk decode across random chunk splits', () { | |
| final rng = Random(12345); | |
| for (var iter = 0; iter < 200; iter++) { | |
| late final List<int> fullPayload; | |
| final encodeSink = chunkedCoding.encoder.startChunkedConversion( | |
| ByteConversionSink.withCallback((bytes) => fullPayload = bytes), | |
| ); | |
| final numChunks = rng.nextInt(5) + 1; | |
| for (var c = 0; c < numChunks; c++) { | |
| final len = rng.nextInt(40); | |
| encodeSink.add(List<int>.generate(len, (_) => rng.nextInt(256))); | |
| } | |
| encodeSink.close(); | |
| final expected = chunkedCoding.decode(fullPayload); | |
| late final List<int> actual; | |
| final decodeSink = chunkedCoding.decoder.startChunkedConversion( | |
| ByteConversionSink.withCallback((bytes) => actual = bytes), | |
| ); | |
| var offset = 0; | |
| while (offset < fullPayload.length) { | |
| final step = rng.nextInt(7); | |
| final end = min(offset + step, fullPayload.length); | |
| decodeSink.addSlice(fullPayload, offset, end, false); | |
| offset = end; | |
| } | |
| decodeSink.close(); | |
| expect(actual, equals(expected)); | |
| } | |
| }); | |
| }); | |
| }); | |
| } |