// Copyright (c) 2015, 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 'package:convert/convert.dart'; import 'package:test/test.dart'; void main() { group('encoder', () { test("doesn't percent-encode unreserved characters", () { var safeChars = 'abcdefghijklmnopqrstuvwxyz' 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789-._~'; expect(percent.encode([...safeChars.codeUnits]), equals(safeChars)); }); test('percent-encodes reserved ASCII characters', () { expect(percent.encode([...' `{@[,/^}\x7f\x00%'.codeUnits]), equals('%20%60%7B%40%5B%2C%2F%5E%7D%7F%00%25')); }); test('percent-encodes non-ASCII characters', () { expect(percent.encode([0x80, 0xFF]), equals('%80%FF')); }); test('mixes encoded and unencoded characters', () { expect(percent.encode([...'a+b=\x80'.codeUnits]), equals('a%2Bb%3D%80')); }); group('with chunked conversion', () { test('percent-encodes byte arrays', () { var results = []; var controller = StreamController(sync: true); controller.stream.listen(results.add); var sink = percent.encoder.startChunkedConversion(controller.sink); sink.add([...'a+b=\x80'.codeUnits]); expect(results, equals(['a%2Bb%3D%80'])); sink.add([0x00, 0x01, 0xfe, 0xff]); expect(results, equals(['a%2Bb%3D%80', '%00%01%FE%FF'])); }); test('handles empty and single-byte lists', () { var results = []; var controller = StreamController(sync: true); controller.stream.listen(results.add); var sink = percent.encoder.startChunkedConversion(controller.sink); sink.add([]); expect(results, equals([''])); sink.add([0x00]); expect(results, equals(['', '%00'])); sink.add([]); expect(results, equals(['', '%00', ''])); }); }); test('rejects non-bytes', () { expect(() => percent.encode([0x100]), throwsFormatException); var sink = percent.encoder.startChunkedConversion(StreamController(sync: true)); expect(() => sink.add([0x100]), throwsFormatException); }); }); group('decoder', () { test('converts percent-encoded strings to byte arrays', () { expect( percent.decode('a%2Bb%3D%801'), equals([...'a+b=\x801'.codeUnits])); }); test('supports lowercase letters', () { expect(percent.decode('a%2bb%3d%80'), equals([...'a+b=\x80'.codeUnits])); }); test('supports more aggressive encoding', () { expect(percent.decode('%61%2E%5A'), equals([...'a.Z'.codeUnits])); }); test('supports less aggressive encoding', () { var chars = ' `{@[,/^}\x7F\x00'; expect(percent.decode(chars), equals([...chars.codeUnits])); }); group('with chunked conversion', () { late List> results; late StringConversionSink sink; setUp(() { results = []; var controller = StreamController>(sync: true); controller.stream.listen(results.add); sink = percent.decoder.startChunkedConversion(controller.sink); }); test('converts percent to byte arrays', () { sink.add('a%2Bb%3D%801'); expect( results, equals([ [...'a+b=\x801'.codeUnits] ])); sink.add('%00%01%FE%FF'); expect( results, equals([ [...'a+b=\x801'.codeUnits], [0x00, 0x01, 0xfe, 0xff] ])); }); test('supports trailing percents and digits split across chunks', () { sink.add('ab%'); expect( results, equals([ [...'ab'.codeUnits] ])); sink.add('2'); expect( results, equals([ [...'ab'.codeUnits] ])); sink.add('0cd%2'); expect( results, equals([ [...'ab'.codeUnits], [...' cd'.codeUnits] ])); sink.add('0'); expect( results, equals([ [...'ab'.codeUnits], [...' cd'.codeUnits], [...' '.codeUnits] ])); }); test('supports empty strings', () { sink.add(''); expect(results, isEmpty); sink.add('%'); expect(results, equals([[]])); sink.add(''); expect(results, equals([[]])); sink.add('2'); expect(results, equals([[]])); sink.add(''); expect(results, equals([[]])); sink.add('0'); expect( results, equals([ [], [0x20] ])); }); test('rejects dangling % detected in close()', () { sink.add('ab%'); expect( results, equals([ [...'ab'.codeUnits] ])); expect(() => sink.close(), throwsFormatException); }); test('rejects dangling digit detected in close()', () { sink.add('ab%2'); expect( results, equals([ [...'ab'.codeUnits] ])); expect(() => sink.close(), throwsFormatException); }); test('rejects danging % detected in addSlice()', () { sink.addSlice('ab%', 0, 3, false); expect( results, equals([ [...'ab'.codeUnits] ])); expect(() => sink.addSlice('ab%', 0, 3, true), throwsFormatException); }); test('rejects danging digit detected in addSlice()', () { sink.addSlice('ab%2', 0, 3, false); expect( results, equals([ [...'ab'.codeUnits] ])); expect(() => sink.addSlice('ab%2', 0, 3, true), throwsFormatException); }); }); group('rejects non-ASCII character', () { for (var char in ['\u0141', '\u{10041}']) { test('"$char"', () { expect(() => percent.decode('a$char'), throwsFormatException); expect(() => percent.decode('${char}a'), throwsFormatException); var sink = percent.decoder .startChunkedConversion(StreamController(sync: true)); expect(() => sink.add(char), throwsFormatException); }); } }); test('rejects % followed by non-hex', () { expect(() => percent.decode('%z2'), throwsFormatException); expect(() => percent.decode('%2z'), throwsFormatException); }); test('rejects dangling % detected in convert()', () { expect(() => percent.decode('ab%'), throwsFormatException); }); test('rejects dangling digit detected in convert()', () { expect(() => percent.decode('ab%2'), throwsFormatException); }); }); }