├── .github ├── dependabot.yaml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── benchmark └── fixed_datetime_formatter_benchmark.dart ├── example └── example.dart ├── lib ├── convert.dart └── src │ ├── accumulator_sink.dart │ ├── byte_accumulator_sink.dart │ ├── charcodes.dart │ ├── codepage.dart │ ├── fixed_datetime_formatter.dart │ ├── hex.dart │ ├── hex │ ├── decoder.dart │ └── encoder.dart │ ├── identity_codec.dart │ ├── percent.dart │ ├── percent │ ├── decoder.dart │ └── encoder.dart │ ├── string_accumulator_sink.dart │ └── utils.dart ├── pubspec.yaml └── test ├── accumulator_sink_test.dart ├── byte_accumulator_sink_test.dart ├── codepage_test.dart ├── fixed_datetime_formatter_test.dart ├── hex_test.dart ├── identity_codec_test.dart ├── percent_test.dart └── string_accumulator_sink_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 26 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release sdk: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.4, dev] 51 | steps: 52 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 53 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - name: Run VM tests 60 | run: dart test --platform vm 61 | if: always() && steps.install.outcome == 'success' 62 | - name: Run Chrome tests 63 | run: dart test --platform chrome 64 | if: always() && steps.install.outcome == 'success' 65 | - name: Run Chrome tests - wasm 66 | run: dart test --platform chrome --compiler dart2wasm 67 | if: always() && steps.install.outcome == 'success' 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.1.2-wip 2 | 3 | - Require Dart 3.4 4 | - Add chunked decoding support (`startChunkedConversion`) for `CodePage` 5 | encodings. 6 | - Upper-cast the return type of the decoder from `List` to `Uint8List`. 7 | 8 | ## 3.1.1 9 | 10 | - Require Dart 2.18 11 | - Fix a number of comment references. 12 | 13 | ## 3.1.0 14 | 15 | - Add a fixed-pattern DateTime formatter. See 16 | [#210](https://github.com/dart-lang/intl/issues/210) in package:intl. 17 | 18 | ## 3.0.2 19 | 20 | - Fix bug in `CodePage` class. See issue 21 | [#47](https://github.com/dart-lang/convert/issues/47). 22 | 23 | ## 3.0.1 24 | 25 | - Dependency clean-up. 26 | 27 | ## 3.0.0 28 | 29 | - Stable null safety release. 30 | - Added `CodePage` class for single-byte `Encoding` implementations. 31 | 32 | ## 2.1.1 33 | 34 | - Fixed a DDC compilation regression for consumers using the Dart 1.x SDK that 35 | was introduced in `2.1.0`. 36 | 37 | ## 2.1.0 38 | 39 | - Added an `IdentityCodec` which implements `Codec` for use as default 40 | value for in functions accepting an optional `Codec` as parameter. 41 | 42 | ## 2.0.2 43 | 44 | - Set max SDK version to `<3.0.0`, and adjust other dependencies. 45 | 46 | ## 2.0.1 47 | 48 | - `PercentEncoder` no longer encodes digits. This follows the specified 49 | behavior. 50 | 51 | ## 2.0.0 52 | 53 | **Note**: No new APIs have been added in 2.0.0. Packages that would use 2.0.0 as 54 | a lower bound should use 1.0.0 instead—for example, `convert: ">=1.0.0 <3.0.0"`. 55 | 56 | - `HexDecoder`, `HexEncoder`, `PercentDecoder`, and `PercentEncoder` no longer 57 | extend `ChunkedConverter`. 58 | 59 | ## 1.1.1 60 | 61 | - Fix all strong-mode warnings. 62 | 63 | ## 1.1.0 64 | 65 | - Add `AccumulatorSink`, `ByteAccumulatorSink`, and `StringAccumulatorSink` 66 | classes for providing synchronous access to the output of chunked converters. 67 | 68 | ## 1.0.1 69 | 70 | - Small improvement in percent decoder efficiency. 71 | 72 | ## 1.0.0 73 | 74 | - Initial version 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/core/tree/main/pkgs/convert 3 | 4 | [![Dart CI](https://github.com/dart-lang/convert/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/convert/actions/workflows/test-package.yml) 5 | [![pub package](https://img.shields.io/pub/v/convert.svg)](https://pub.dev/packages/convert) 6 | [![package publisher](https://img.shields.io/pub/publisher/convert.svg)](https://pub.dev/packages/convert/publisher) 7 | 8 | Contains encoders and decoders for converting between different 9 | data representations. It's the external counterpart of the 10 | [`dart:convert`](https://api.dart.dev/dart-convert/dart-convert-library.html) 11 | SDK library, and contains less-central APIs and APIs that need more flexible 12 | versioning. 13 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-raw-types: true 8 | 9 | linter: 10 | rules: 11 | - avoid_private_typedef_functions 12 | - avoid_redundant_argument_values 13 | - avoid_unused_constructor_parameters 14 | - avoid_void_async 15 | - cancel_subscriptions 16 | - literal_only_boolean_expressions 17 | - missing_whitespace_between_adjacent_strings 18 | - no_adjacent_strings_in_list 19 | - no_runtimeType_toString 20 | - package_api_docs 21 | - prefer_const_declarations 22 | - prefer_expression_function_bodies 23 | - unnecessary_await_in_return 24 | - use_string_buffers 25 | -------------------------------------------------------------------------------- /benchmark/fixed_datetime_formatter_benchmark.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:benchmark_harness/benchmark_harness.dart'; 6 | import 'package:convert/convert.dart'; 7 | 8 | /// Test the performance of [FixedDateTimeFormatter.decode]. 9 | class DecodeBenchmark extends BenchmarkBase { 10 | final fixedDateTimeFormatter = FixedDateTimeFormatter('YYYYMMDDhhmmss'); 11 | DecodeBenchmark() : super('Parse 10k strings to DateTime'); 12 | 13 | @override 14 | void run() { 15 | for (var i = 0; i < 10000; i++) { 16 | fixedDateTimeFormatter.decode('19960425050322'); 17 | } 18 | } 19 | } 20 | 21 | void main() { 22 | DecodeBenchmark().report(); 23 | } 24 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:convert/convert.dart'; 8 | 9 | void main(List args) { 10 | // Creates a Codec that converts a UTF-8 strings to/from percent encoding 11 | final fusedCodec = utf8.fuse(percent); 12 | 13 | final input = args.isNotEmpty ? args.first : 'ABC 123 @!('; 14 | print(input); 15 | final encodedMessage = fusedCodec.encode(input); 16 | print(encodedMessage); 17 | 18 | final decodedMessage = fusedCodec.decode(encodedMessage); 19 | assert(decodedMessage == input); 20 | } 21 | -------------------------------------------------------------------------------- /lib/convert.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/accumulator_sink.dart'; 6 | export 'src/byte_accumulator_sink.dart'; 7 | export 'src/codepage.dart'; 8 | export 'src/fixed_datetime_formatter.dart'; 9 | export 'src/hex.dart'; 10 | export 'src/identity_codec.dart'; 11 | export 'src/percent.dart'; 12 | export 'src/string_accumulator_sink.dart'; 13 | -------------------------------------------------------------------------------- /lib/src/accumulator_sink.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | import 'dart:convert'; 7 | 8 | /// A sink that provides access to all the [events] that have been passed to it. 9 | /// 10 | /// See also [ChunkedConversionSink.withCallback]. 11 | class AccumulatorSink implements Sink { 12 | /// An unmodifiable list of events passed to this sink so far. 13 | List get events => UnmodifiableListView(_events); 14 | final _events = []; 15 | 16 | /// Whether [close] has been called. 17 | bool get isClosed => _isClosed; 18 | var _isClosed = false; 19 | 20 | /// Removes all events from [events]. 21 | /// 22 | /// This can be used to avoid double-processing events. 23 | void clear() { 24 | _events.clear(); 25 | } 26 | 27 | @override 28 | void add(T event) { 29 | if (_isClosed) { 30 | throw StateError("Can't add to a closed sink."); 31 | } 32 | 33 | _events.add(event); 34 | } 35 | 36 | @override 37 | void close() { 38 | _isClosed = true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/byte_accumulator_sink.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:typed_data'; 7 | 8 | import 'package:typed_data/typed_data.dart'; 9 | 10 | /// A sink that provides access to the concatenated bytes passed to it. 11 | /// 12 | /// See also [ByteConversionSink.withCallback]. 13 | class ByteAccumulatorSink extends ByteConversionSinkBase { 14 | /// The bytes accumulated so far. 15 | /// 16 | /// The returned [Uint8List] is viewing a shared buffer, so it should not be 17 | /// changed and any bytes outside the view should not be accessed. 18 | Uint8List get bytes => Uint8List.view(_buffer.buffer, 0, _buffer.length); 19 | 20 | final _buffer = Uint8Buffer(); 21 | 22 | /// Whether [close] has been called. 23 | bool get isClosed => _isClosed; 24 | var _isClosed = false; 25 | 26 | /// Removes all bytes from [bytes]. 27 | /// 28 | /// This can be used to avoid double-processing data. 29 | void clear() { 30 | _buffer.clear(); 31 | } 32 | 33 | @override 34 | void add(List chunk) { 35 | if (_isClosed) { 36 | throw StateError("Can't add to a closed sink."); 37 | } 38 | 39 | _buffer.addAll(chunk); 40 | } 41 | 42 | @override 43 | void addSlice(List chunk, int start, int end, bool isLast) { 44 | if (_isClosed) { 45 | throw StateError("Can't add to a closed sink."); 46 | } 47 | 48 | _buffer.addAll(chunk, start, end); 49 | if (isLast) _isClosed = true; 50 | } 51 | 52 | @override 53 | void close() { 54 | _isClosed = true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/charcodes.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Character `%`. 6 | const int $percent = 0x25; 7 | 8 | /// Character `-`. 9 | const int $dash = 0x2d; 10 | 11 | /// Character `.`. 12 | const int $dot = 0x2e; 13 | 14 | /// Character `0`. 15 | const int $0 = 0x30; 16 | 17 | /// Character `9`. 18 | const int $9 = 0x39; 19 | 20 | /// Character `A`. 21 | const int $A = 0x41; 22 | 23 | /// Character `_`. 24 | const int $underscore = 0x5f; 25 | 26 | /// Character `a`. 27 | const int $a = 0x61; 28 | 29 | /// Character `f`. 30 | const int $f = 0x66; 31 | 32 | /// Character `z`. 33 | const int $z = 0x7a; 34 | 35 | /// Character `~`. 36 | const int $tilde = 0x7e; 37 | -------------------------------------------------------------------------------- /lib/src/codepage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:typed_data'; 7 | 8 | /// The ISO-8859-2/Latin-2 (Eastern European) code page. 9 | final CodePage latin2 = 10 | CodePage._bmp('latin-2', '$_ascii$_noControls$_top8859_2'); 11 | 12 | /// The ISO-8859-3/Latin-3 (South European) code page. 13 | final CodePage latin3 = 14 | CodePage._bmp('latin-3', '$_ascii$_noControls$_top8859_3'); 15 | 16 | /// The ISO-8859-4/Latin-4 (North European) code page. 17 | final CodePage latin4 = 18 | CodePage._bmp('latin-4', '$_ascii$_noControls$_top8859_4'); 19 | 20 | /// The ISO-8859-5/Latin-Cyrillic code page. 21 | final CodePage latinCyrillic = 22 | CodePage._bmp('cyrillic', '$_ascii$_noControls$_top8859_5'); 23 | 24 | /// The ISO-8859-6/Latin-Arabic code page. 25 | final CodePage latinArabic = 26 | CodePage._bmp('arabic', '$_ascii$_noControls$_top8859_6'); 27 | 28 | /// The ISO-8859-7/Latin-Greek code page. 29 | final CodePage latinGreek = 30 | CodePage._bmp('greek', '$_ascii$_noControls$_top8859_7'); 31 | 32 | /// The ISO-8859-7/Latin-Hebrew code page. 33 | final CodePage latinHebrew = 34 | CodePage._bmp('hebrew', '$_ascii$_noControls$_top8859_8'); 35 | 36 | /// The ISO-8859-9/Latin-5 (Turkish) code page. 37 | final CodePage latin5 = 38 | CodePage._bmp('latin-5', '$_ascii$_noControls$_top8859_9'); 39 | 40 | /// The ISO-8859-10/Latin-6 (Nordic) code page. 41 | final CodePage latin6 = 42 | CodePage._bmp('latin-6', '$_ascii$_noControls$_top8859_10'); 43 | 44 | /// The ISO-8859-11/Latin-Thai code page. 45 | final CodePage latinThai = 46 | CodePage._bmp('tis620', '$_ascii$_noControls$_top8859_11'); 47 | 48 | /// The ISO-8859-13/Latin-6 (Baltic Rim) code page. 49 | final CodePage latin7 = 50 | CodePage._bmp('latin-7', '$_ascii$_noControls$_top8859_13'); 51 | 52 | /// The ISO-8859-14/Latin-8 (Celtic) code page. 53 | final CodePage latin8 = 54 | CodePage._bmp('latin-8', '$_ascii$_noControls$_top8859_14'); 55 | 56 | /// The ISO-8859-15/Latin-9 (Western European revised) code page. 57 | final CodePage latin9 = 58 | CodePage._bmp('latin-9', '$_ascii$_noControls$_top8859_15'); 59 | 60 | /// The ISO-8859-16/Latin-10 (South Eastern European) code page. 61 | final CodePage latin10 = 62 | CodePage._bmp('latin-10', '$_ascii$_noControls$_top8859_16'); 63 | 64 | /// Characters in ISO-8859-2 above the ASCII and top control characters. 65 | const _top8859_2 = '\xa0Ą˘Ł¤ĽŚ§¨ŠŞŤŹ\xadŽŻ°ą˛ł´ľśˇ¸šşťź˝žż' 66 | 'ŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢß' 67 | 'ŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙'; 68 | 69 | /// Characters in ISO-8859-3 above the ASCII and top control characters. 70 | const _top8859_3 = '\xa0Ħ˘£\uFFFD¤Ĥ§¨İŞĞĴ\xad\uFFFDݰħ²³´µĥ·¸ışğĵ½\uFFFDż' 71 | 'ÀÁÂ\uFFFDÄĊĈÇÈÉÊËÌÍÎÏ\uFFFDÑÒÓÔĠÖ×ĜÙÚÛÜŬŜß' 72 | 'àáâ\uFFFDäċĉçèéêëìíîï\uFFFDñòóôġö÷ĝùúûüŭŝ˙'; 73 | 74 | /// Characters in ISO-8859-4 above the ASCII and top control characters. 75 | const _top8859_4 = '\xa0ĄĸŖ¤Ĩϧ¨ŠĒĢŦ\xadޝ°ą˛ŗ´ĩšēģŧŊžŋ' 76 | 'ĀÁÂÃÄÅÆĮČÉĘËĖÍÎĪĐŅŌĶÔÕÖרŲÚÛÜŨŪß' 77 | 'āáâãäåæįčéęëėíîīđņōķôõö÷øųúûüũū˙'; 78 | 79 | /// Characters in ISO-8859-5 above the ASCII and top control characters. 80 | const _top8859_5 = '\xa0ЁЂЃЄЅІЇЈЉЊЋЌ\xadЎЏАБВГДЕЖЗИЙКЛМНОП' 81 | 'РСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмноп' 82 | 'рстуфхцчшщъыьэюя№ёђѓєѕіїјљњћќ§ўџ'; 83 | 84 | /// Characters in ISO-8859-6 above the ASCII and top control characters. 85 | const _top8859_6 = '\xa0\uFFFD\uFFFD\uFFFD¤\uFFFD\uFFFD\uFFFD' 86 | '\uFFFD\uFFFD\uFFFD\uFFFD\u060c\xad\uFFFD\uFFFD' 87 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 88 | '\uFFFD\uFFFD\uFFFD\u061b\uFFFD\uFFFD\uFFFD\u061f' 89 | '\uFFFD\u0621\u0622\u0623\u0624\u0625\u0626\u0627' 90 | '\u0628\u0629\u062a\u062b\u062c\u062d\u062e\u062f' 91 | '\u0630\u0631\u0632\u0633\u0634\u0635\u0636\u0637' 92 | '\u0638\u0639\u063a\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 93 | '\u0640\u0641\u0642\u0643\u0644\u0645\u0646\u0647' 94 | '\u0648\u0649\u064a\u064b\u064c\u064d\u064e\u064f' 95 | '\u0650\u0651\u0652\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 96 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD'; 97 | 98 | /// Characters in ISO-8859-7 above the ASCII and top control characters. 99 | const _top8859_7 = '\xa0‘’£€₯¦§¨©ͺ«¬\xad\uFFFD―°±²³΄΅Ά·ΈΉΊ»Ό½ΎΏ' 100 | 'ΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡ\uFFFDΣΤΥΦΧΨΩΪΫάέήί' 101 | 'ΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώ\uFFFD'; 102 | 103 | /// Characters in ISO-8859-8 above the ASCII and top control characters. 104 | const _top8859_8 = '\xa0\uFFFD¢£¤¥¦§¨©×«¬\xad®¯°±²³´µ¶·¸¹÷»¼½¾\uFFFD' 105 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 106 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 107 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 108 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD‗' 109 | '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7' 110 | '\u05d8\u05d9\u05da\u05db\u05dc\u05dd\u05de\u05df' 111 | '\u05e0\u05e1\u05e2\u05e3\u05e4\u05e5\u05e6\u05e7' 112 | '\u05e8\u05e9\u05ea\uFFFD\uFFFD\u200e\u200f\uFFFD'; 113 | 114 | /// Characters in ISO-8859-9 above the ASCII and top control characters. 115 | const _top8859_9 = '\xa0¡¢£¤¥¦§¨©ª«¬\xad®¯°±²³´µ¶·¸¹º»¼½¾¿' 116 | 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏĞÑÒÓÔÕÖרÙÚÛÜİŞß' 117 | 'àáâãäåæçèéêëìíîïğñòóôõö÷øùúûüışÿ'; 118 | 119 | /// Characters in ISO-8859-10 above the ASCII and top control characters. 120 | const _top8859_10 = '\xa0ĄĒĢĪĨͧĻĐŠŦŽ\xadŪŊ°ąēģīĩķ·ļđšŧž―ūŋ' 121 | 'ĀÁÂÃÄÅÆĮČÉĘËĖÍÎÏÐŅŌÓÔÕÖŨØŲÚÛÜÝÞß' 122 | 'āáâãäåæįčéęëėíîïðņōóôõöũøųúûüýþĸ'; 123 | 124 | /// Characters in ISO-8859-11 above the ASCII and top control characters. 125 | const _top8859_11 = '\xa0กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟ' 126 | 'ภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู\uFFFD\uFFFD\uFFFD\uFFFD฿' 127 | 'เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛\uFFFD\uFFFD\uFFFD\uFFFD'; 128 | 129 | /// Characters in ISO-8859-13 above the ASCII and top control characters. 130 | const _top8859_13 = '\xa0”¢£¤„¦§Ø©Ŗ«¬\xad®Æ°±²³“µ¶·ø¹ŗ»¼½¾æ' 131 | 'ĄĮĀĆÄÅĘĒČÉŹĖĢĶĪĻŠŃŅÓŌÕÖ×ŲŁŚŪÜŻŽß' 132 | 'ąįāćäåęēčéźėģķīļšńņóōõö÷ųłśūüżž’'; 133 | 134 | /// Characters in ISO-8859-14 above the ASCII and top control characters. 135 | const _top8859_14 = '\xa0Ḃḃ£ĊċḊ§Ẁ©ẂḋỲ\xad®ŸḞḟĠġṀṁ¶ṖẁṗẃṠỳẄẅṡ' 136 | 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏŴÑÒÓÔÕÖṪØÙÚÛÜÝŶß' 137 | 'àáâãäåæçèéêëìíîïŵñòóôõöṫøùúûüýŷÿ'; 138 | 139 | /// Characters in ISO-8859-15 above the ASCII and top control characters. 140 | const _top8859_15 = '\xa0¡¢£€¥Š§š©ª«¬\xad®¯°±²³Žµ¶·ž¹º»ŒœŸ¿' 141 | 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞß' 142 | 'àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ'; 143 | 144 | /// Characters in ISO-8859-16 above the ASCII and top control characters. 145 | const _top8859_16 = '\xa0ĄąŁ€„Чš©Ș«Ź\xadźŻ°±ČłŽ”¶·žčș»ŒœŸż' 146 | 'ÀÁÂĂÄĆÆÇÈÉÊËÌÍÎÏĐŃÒÓÔŐÖŚŰÙÚÛÜĘȚß' 147 | 'àáâăäćæçèéêëìíîïđńòóôőöśűùúûüęțÿ'; 148 | 149 | const _noControls = '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 150 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 151 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD' 152 | '\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD'; 153 | 154 | /// ASCII characters without control characters. Shared by many code pages. 155 | const _ascii = '$_noControls' 156 | // ignore: missing_whitespace_between_adjacent_strings 157 | r""" !"#$%&'()*+,-./0123456789:;<=>?""" 158 | r'@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_' 159 | '`abcdefghijklmnopqrstuvwxyz{|}~\uFFFD'; 160 | 161 | /// A mapping between bytes and characters. 162 | /// 163 | /// A code page is a way to map bytes to character. 164 | /// As such, it can only represent 256 different characters. 165 | class CodePage extends Encoding { 166 | @override 167 | final CodePageDecoder decoder; 168 | @override 169 | final String name; 170 | CodePageEncoder? _encoder; 171 | 172 | /// Creates a code page with the given name and characters. 173 | /// 174 | /// The [characters] string must contain 256 code points (runes) 175 | /// in the order of the bytes representing them. 176 | /// 177 | /// Any byte not defined by the code page should have a 178 | /// U+FFFD (invalid character) code point at its place in 179 | /// [characters]. 180 | /// 181 | /// The name is used by [Encoding.name]. 182 | factory CodePage(String name, String characters) = CodePage._general; 183 | 184 | /// Creates a code page with the characters of [characters]. 185 | /// 186 | /// The [characters] must contain precisely 256 characters (code points). 187 | /// 188 | /// A U+FFFD (invalid character) entry in [characters] means that the 189 | /// corresponding byte does not have a definition in this code page. 190 | CodePage._general(this.name, String characters) 191 | : decoder = _createDecoder(characters); 192 | 193 | /// Creates a code page with characters from the basic multilingual plane. 194 | /// 195 | /// The basic multilingual plane (BMP) contains the first 65536 code points. 196 | /// As such, each character can be represented by a single UTF-16 code unit, 197 | /// which makes some operations more efficient. 198 | /// 199 | /// The [characters] must contain precisely 256 code points from the BMP 200 | /// which means that it should have length 256 and not contain any surrogates. 201 | /// 202 | /// A U+FFFD (invalid character) entry in [characters] means that the 203 | /// corresponding byte does not have a definition in this code page. 204 | CodePage._bmp(this.name, String characters) 205 | : decoder = _BmpCodePageDecoder(characters); 206 | 207 | /// The character associated with a particular byte in this code page. 208 | /// 209 | /// The [byte] must be in the range 0..255. 210 | /// The returned value should be a Unicode scalar value 211 | /// (a non-surrogate code point). 212 | /// 213 | /// If a code page does not have a defined character for a particular 214 | /// byte, it should return the Unicode invalid character (U+FFFD) 215 | /// instad. 216 | int operator [](int byte) => decoder._char(byte); 217 | 218 | /// Encodes [input] using `encoder.convert`. 219 | @override 220 | Uint8List encode(String input, {int? invalidCharacter}) => 221 | encoder.convert(input, invalidCharacter: invalidCharacter); 222 | 223 | /// Decodes [bytes] using `encoder.convert`. 224 | @override 225 | String decode(List bytes, {bool allowInvalid = false}) => 226 | decoder.convert(bytes, allowInvalid: allowInvalid); 227 | 228 | @override 229 | CodePageEncoder get encoder => _encoder ??= decoder._createEncoder(); 230 | } 231 | 232 | /// A code page decoder, converts from bytes to characters. 233 | /// 234 | /// A code page assigns characters to a subset of byte values. 235 | /// The decoder converts those bytes back to their characters. 236 | abstract class CodePageDecoder implements Converter, String> { 237 | /// Decodes a sequence of bytes into a string using a code page. 238 | /// 239 | /// The code page assigns one character to each byte. 240 | /// Values in [input] must be bytes (integers in the range 0..255). 241 | /// 242 | /// If [allowInvalid] is true, non-byte values in [input], 243 | /// or byte values not defined as a character in the code page, 244 | /// are emitted as U+FFFD (the Unicode invalid character). 245 | /// If not true, the bytes must be calid and defined characters. 246 | @override 247 | String convert(List input, {bool allowInvalid = false}); 248 | 249 | CodePageEncoder _createEncoder(); 250 | int _char(int byte); 251 | } 252 | 253 | /// Creates a decoder from [characters]. 254 | /// 255 | /// Recognizes if [characters] contains only characters in the BMP, 256 | /// and creates a [_BmpCodePageDecoder] in that case. 257 | CodePageDecoder _createDecoder(String characters) { 258 | var result = Uint32List(256); 259 | var i = 0; 260 | var allChars = 0; 261 | for (var char in characters.runes) { 262 | if (i >= 256) { 263 | throw ArgumentError.value( 264 | characters, 'characters', 'Must contain 256 characters'); 265 | } 266 | result[i++] = char; 267 | allChars |= char; 268 | } 269 | if (i < 256) { 270 | throw ArgumentError.value( 271 | characters, 'characters', 'Must contain 256 characters'); 272 | } 273 | if (allChars <= 0xFFFF) { 274 | // It's in the BMP. 275 | return _BmpCodePageDecoder(characters); 276 | } 277 | return _NonBmpCodePageDecoder._(result); 278 | } 279 | 280 | /// An input [ByteConversionSink] for decoders where each input byte can be be 281 | /// considered independantly. 282 | class _CodePageDecoderSink extends ByteConversionSink { 283 | final Sink _output; 284 | final Converter, String> _decoder; 285 | 286 | _CodePageDecoderSink(this._output, this._decoder); 287 | 288 | @override 289 | void add(List chunk) { 290 | _output.add(_decoder.convert(chunk)); 291 | } 292 | 293 | @override 294 | void close() { 295 | _output.close(); 296 | } 297 | } 298 | 299 | /// Code page with non-BMP characters. 300 | class _NonBmpCodePageDecoder extends Converter, String> 301 | implements CodePageDecoder { 302 | final Uint32List _characters; 303 | _NonBmpCodePageDecoder(String characters) : this._(_buildMapping(characters)); 304 | _NonBmpCodePageDecoder._(this._characters); 305 | 306 | @override 307 | int _char(int byte) => _characters[byte]; 308 | 309 | static Uint32List _buildMapping(String characters) { 310 | var result = Uint32List(256); 311 | var i = 0; 312 | for (var char in characters.runes) { 313 | if (i >= 256) { 314 | throw ArgumentError.value( 315 | characters, 'characters', 'Must contain 256 characters'); 316 | } 317 | result[i++] = char; 318 | } 319 | if (i < 256) { 320 | throw ArgumentError.value( 321 | characters, 'characters', 'Must contain 256 characters'); 322 | } 323 | return result; 324 | } 325 | 326 | @override 327 | CodePageEncoder _createEncoder() { 328 | var result = {}; 329 | for (var i = 0; i < 256; i++) { 330 | var char = _characters[i]; 331 | if (char != 0xFFFD) { 332 | result[char] = i; 333 | } 334 | } 335 | return CodePageEncoder._(result); 336 | } 337 | 338 | @override 339 | String convert(List input, {bool allowInvalid = false}) { 340 | var buffer = Uint32List(input.length); 341 | for (var i = 0; i < input.length; i++) { 342 | var byte = input[i]; 343 | if (byte & 0xff != byte) throw FormatException('Not a byte', input, i); 344 | buffer[i] = _characters[byte]; 345 | } 346 | return String.fromCharCodes(buffer); 347 | } 348 | 349 | @override 350 | Sink> startChunkedConversion(Sink sink) => 351 | _CodePageDecoderSink(sink, this); 352 | } 353 | 354 | class _BmpCodePageDecoder extends Converter, String> 355 | implements CodePageDecoder { 356 | final String _characters; 357 | _BmpCodePageDecoder(String characters) : _characters = characters { 358 | if (characters.length != 256) { 359 | throw ArgumentError.value(characters, 'characters', 360 | 'Must contain 256 characters. Was ${characters.length}'); 361 | } 362 | } 363 | 364 | @override 365 | int _char(int byte) => _characters.codeUnitAt(byte); 366 | 367 | @override 368 | String convert(List bytes, {bool allowInvalid = false}) { 369 | if (allowInvalid) return _convertAllowInvalid(bytes); 370 | var count = bytes.length; 371 | var codeUnits = Uint16List(count); 372 | for (var i = 0; i < count; i++) { 373 | var byte = bytes[i]; 374 | if (byte != byte & 0xff) { 375 | throw FormatException('Not a byte value', bytes, i); 376 | } 377 | var character = _characters.codeUnitAt(byte); 378 | if (character == 0xFFFD) { 379 | throw FormatException('Not defined in this code page', bytes, i); 380 | } 381 | codeUnits[i] = character; 382 | } 383 | return String.fromCharCodes(codeUnits); 384 | } 385 | 386 | @override 387 | Sink> startChunkedConversion(Sink sink) => 388 | _CodePageDecoderSink(sink, this); 389 | 390 | String _convertAllowInvalid(List bytes) { 391 | var count = bytes.length; 392 | var codeUnits = Uint16List(count); 393 | for (var i = 0; i < count; i++) { 394 | var byte = bytes[i]; 395 | int character; 396 | if (byte == byte & 0xff) { 397 | character = _characters.codeUnitAt(byte); 398 | } else { 399 | character = 0xFFFD; 400 | } 401 | codeUnits[i] = character; 402 | } 403 | return String.fromCharCodes(codeUnits); 404 | } 405 | 406 | @override 407 | CodePageEncoder _createEncoder() => CodePageEncoder._bmp(_characters); 408 | } 409 | 410 | /// Encoder for a code page. 411 | /// 412 | /// Converts a string into bytes where each byte represents that character 413 | /// according to the code page definition. 414 | class CodePageEncoder extends Converter> { 415 | final Map _encoding; 416 | 417 | CodePageEncoder._bmp(String characters) 418 | : _encoding = _createBmpEncoding(characters); 419 | 420 | CodePageEncoder._(this._encoding); 421 | 422 | static Map _createBmpEncoding(String characters) { 423 | var encoding = {}; 424 | for (var i = 0; i < characters.length; i++) { 425 | var char = characters.codeUnitAt(i); 426 | if (char != 0xFFFD) encoding[characters.codeUnitAt(i)] = i; 427 | } 428 | return encoding; 429 | } 430 | 431 | /// Converts input to the byte encoding in this code page. 432 | /// 433 | /// If [invalidCharacter] is supplied, it must be a byte value 434 | /// (in the range 0..255). 435 | /// 436 | /// If [input] contains characters that are not available 437 | /// in this code page, they are replaced by the [invalidCharacter] byte, 438 | /// and then [invalidCharacter] must have been supplied. 439 | @override 440 | Uint8List convert(String input, {int? invalidCharacter}) { 441 | if (invalidCharacter != null) { 442 | RangeError.checkValueInInterval( 443 | invalidCharacter, 0, 255, 'invalidCharacter'); 444 | } 445 | var count = input.length; 446 | var result = Uint8List(count); 447 | var j = 0; 448 | for (var i = 0; i < count; i++) { 449 | var char = input.codeUnitAt(i); 450 | var byte = _encoding[char]; 451 | nullCheck: 452 | if (byte == null) { 453 | // Check for surrogate. 454 | var offset = i; 455 | if (char & 0xFC00 == 0xD800 && i + 1 < count) { 456 | var next = input.codeUnitAt(i + 1); 457 | if ((next & 0xFC00) == 0xDC00) { 458 | i = i + 1; 459 | char = 0x10000 + ((char & 0x3ff) << 10) + (next & 0x3ff); 460 | byte = _encoding[char]; 461 | if (byte != null) break nullCheck; 462 | } 463 | } 464 | byte = invalidCharacter ?? 465 | (throw FormatException( 466 | 'Not a character in this code page', input, offset)); 467 | } 468 | result[j++] = byte; 469 | } 470 | return Uint8List.sublistView(result, 0, j); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /lib/src/fixed_datetime_formatter.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// A formatter and parser for [DateTime] in a fixed format [String] pattern. 6 | /// 7 | /// For example, calling 8 | /// `FixedDateTimeCodec('YYYYMMDDhhmmss').decodeToLocal('19960425050322')` has 9 | /// the same result as calling `DateTime(1996, 4, 25, 5, 3, 22)`. 10 | /// 11 | /// The allowed characters are 12 | /// * `Y` for “calendar year” 13 | /// * `M` for “calendar month” 14 | /// * `D` for “calendar day” 15 | /// * `E` for “decade” 16 | /// * `C` for “century” 17 | /// * `h` for “clock hour” 18 | /// * `m` for “clock minute” 19 | /// * `s` for “clock second” 20 | /// * `S` for “fractional clock second” 21 | /// 22 | /// Note: Negative years are not supported. 23 | /// 24 | /// Non-allowed characters in the format [pattern] are included when decoding a 25 | /// string, in this case `YYYY kiwi MM` is the same format string as 26 | /// `YYYY------MM`. When encoding a [DateTime], the non-format characters are in 27 | /// the output verbatim. 28 | /// 29 | /// Note: this class differs from 30 | /// [DateFormat](https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html) 31 | /// from [package:intl](https://pub.dev/packages/intl) in that here, the format 32 | /// character count is interpreted literally. For example, using the format 33 | /// string `YYY` to decode the string `996` would result in the same [DateTime] 34 | /// as calling `DateTime(996)`, and the same format string used to encode the 35 | /// `DateTime(1996)` would output only the three digits 996. 36 | class FixedDateTimeFormatter { 37 | static const _powersOfTen = [1, 10, 100, 1000, 10000, 100000]; 38 | static const _validFormatCharacters = [ 39 | _yearCode, 40 | _monthCode, 41 | _dayCode, 42 | _decadeCode, 43 | _centuryCode, 44 | _hourCode, 45 | _minuteCode, 46 | _secondCode, 47 | _fractionSecondCode, 48 | ]; 49 | static const _yearCode = 0x59; /*Y*/ 50 | static const _monthCode = 0x4D; /*M*/ 51 | static const _dayCode = 0x44; /*D*/ 52 | static const _decadeCode = 0x45; /*E*/ 53 | static const _centuryCode = 0x43; /*C*/ 54 | static const _hourCode = 0x68; /*H*/ 55 | static const _minuteCode = 0x6D; /*m*/ 56 | static const _secondCode = 0x73; /*s*/ 57 | static const _fractionSecondCode = 0x53; /*S*/ 58 | 59 | /// The format pattern string of this formatter. 60 | final String pattern; 61 | 62 | /// Whether to create UTC [DateTime] objects when parsing. 63 | /// 64 | /// If not, the created [DateTime] objects are in the local time zone. 65 | final bool isUtc; 66 | 67 | final _blocks = _ParsedFormatBlocks(); 68 | 69 | /// Creates a new [FixedDateTimeFormatter] with the provided [pattern]. 70 | /// 71 | /// The [pattern] interprets the characters mentioned in 72 | /// [FixedDateTimeFormatter] to represent fields of a `DateTime` value. Other 73 | /// characters are not special. If [isUtc] is set to false, the DateTime is 74 | /// constructed with respect to the local timezone. 75 | /// 76 | /// There must at most be one sequence of each special character to ensure a 77 | /// single source of truth when constructing the [DateTime], so a pattern of 78 | /// `"CCCC-MM-DD, CC"` is invalid, because it has two separate `C` sequences. 79 | FixedDateTimeFormatter(this.pattern, {this.isUtc = true}) { 80 | int? currentCharacter; 81 | var start = 0; 82 | for (var i = 0; i < pattern.length; i++) { 83 | var formatCharacter = pattern.codeUnitAt(i); 84 | if (currentCharacter != formatCharacter) { 85 | _blocks.saveBlock(currentCharacter, start, i); 86 | if (_validFormatCharacters.contains(formatCharacter)) { 87 | var hasSeenBefore = _blocks.formatCharacters.indexOf(formatCharacter); 88 | if (hasSeenBefore > -1) { 89 | throw FormatException( 90 | "Pattern contains more than one '$formatCharacter' block.\n" 91 | 'Previous occurrence at index ${_blocks.starts[hasSeenBefore]}', 92 | pattern, 93 | i); 94 | } else { 95 | start = i; 96 | currentCharacter = formatCharacter; 97 | } 98 | } else { 99 | currentCharacter = null; 100 | } 101 | } 102 | } 103 | _blocks.saveBlock(currentCharacter, start, pattern.length); 104 | } 105 | 106 | /// Converts a [DateTime] to a [String] as specified by the [pattern]. 107 | /// 108 | /// The [DateTime.year] must not be negative. 109 | String encode(DateTime dateTime) { 110 | if (dateTime.year < 0) { 111 | throw ArgumentError.value( 112 | dateTime, 113 | 'dateTime', 114 | 'Year must not be negative', 115 | ); 116 | } 117 | var buffer = StringBuffer(); 118 | for (var i = 0; i < _blocks.length; i++) { 119 | var start = _blocks.starts[i]; 120 | var end = _blocks.ends[i]; 121 | var length = end - start; 122 | 123 | var previousEnd = i > 0 ? _blocks.ends[i - 1] : 0; 124 | if (previousEnd < start) { 125 | buffer.write(pattern.substring(previousEnd, start)); 126 | } 127 | var formatCharacter = _blocks.formatCharacters[i]; 128 | var number = _extractNumberFromDateTime( 129 | formatCharacter, 130 | dateTime, 131 | length, 132 | ); 133 | if (number.length > length) { 134 | number = number.substring(number.length - length); 135 | } else if (length > number.length) { 136 | number = number.padLeft(length, '0'); 137 | } 138 | buffer.write(number); 139 | } 140 | if (_blocks.length > 0) { 141 | var lastEnd = _blocks.ends.last; 142 | if (lastEnd < pattern.length) { 143 | buffer.write(pattern.substring(lastEnd, pattern.length)); 144 | } 145 | } 146 | return buffer.toString(); 147 | } 148 | 149 | String _extractNumberFromDateTime( 150 | int? formatCharacter, 151 | DateTime dateTime, 152 | int length, 153 | ) { 154 | int value; 155 | switch (formatCharacter) { 156 | case _yearCode: 157 | value = dateTime.year; 158 | break; 159 | case _centuryCode: 160 | value = dateTime.year ~/ 100; 161 | break; 162 | case _decadeCode: 163 | value = dateTime.year ~/ 10; 164 | break; 165 | case _monthCode: 166 | value = dateTime.month; 167 | break; 168 | case _dayCode: 169 | value = dateTime.day; 170 | break; 171 | case _hourCode: 172 | value = dateTime.hour; 173 | break; 174 | case _minuteCode: 175 | value = dateTime.minute; 176 | break; 177 | case _secondCode: 178 | value = dateTime.second; 179 | break; 180 | case _fractionSecondCode: 181 | value = dateTime.millisecond; 182 | switch (length) { 183 | case 1: 184 | value ~/= 100; 185 | break; 186 | case 2: 187 | value ~/= 10; 188 | break; 189 | case 3: 190 | break; 191 | case 4: 192 | value = value * 10 + dateTime.microsecond ~/ 100; 193 | break; 194 | case 5: 195 | value = value * 100 + dateTime.microsecond ~/ 10; 196 | break; 197 | case 6: 198 | value = value * 1000 + dateTime.microsecond; 199 | break; 200 | default: 201 | throw AssertionError( 202 | 'Unreachable, length is restricted to 6 in the constructor'); 203 | } 204 | break; 205 | default: 206 | throw AssertionError( 207 | 'Unreachable, the key is checked in the constructor'); 208 | } 209 | return value.toString().padLeft(length, '0'); 210 | } 211 | 212 | /// Parses [formattedDateTime] to a [DateTime] as specified by the [pattern]. 213 | /// 214 | /// Parts of a [DateTime] which are not mentioned in the pattern default to a 215 | /// value of zero for time parts and year, and a value of 1 for day and month. 216 | /// 217 | /// Throws a [FormatException] if the [formattedDateTime] does not match the 218 | /// [pattern]. 219 | DateTime decode(String formattedDateTime) => 220 | _decode(formattedDateTime, isUtc, true)!; 221 | 222 | /// Parses [formattedDateTime] to a [DateTime] as specified by the [pattern]. 223 | /// 224 | /// Parts of a [DateTime] which are not mentioned in the pattern default to a 225 | /// value of zero for time parts and year, and a value of 1 for day and month. 226 | /// 227 | /// Returns the parsed value, or `null` if the [formattedDateTime] does not 228 | /// match the [pattern]. 229 | DateTime? tryDecode(String formattedDateTime) => 230 | _decode(formattedDateTime, isUtc, false); 231 | 232 | DateTime? _decode( 233 | String formattedDateTime, 234 | bool isUtc, 235 | bool throwOnError, 236 | ) { 237 | var year = 0; 238 | var month = 1; 239 | var day = 1; 240 | var hour = 0; 241 | var minute = 0; 242 | var second = 0; 243 | var microsecond = 0; 244 | for (var i = 0; i < _blocks.length; i++) { 245 | var formatCharacter = _blocks.formatCharacters[i]; 246 | var number = _extractNumberFromString(formattedDateTime, i, throwOnError); 247 | if (number != null) { 248 | if (formatCharacter == _fractionSecondCode) { 249 | // Special case, as we want fractional seconds to be the leading 250 | // digits. 251 | number *= _powersOfTen[6 - (_blocks.ends[i] - _blocks.starts[i])]; 252 | } 253 | switch (formatCharacter) { 254 | case _yearCode: 255 | year += number; 256 | break; 257 | case _centuryCode: 258 | year += number * 100; 259 | break; 260 | case _decadeCode: 261 | year += number * 10; 262 | break; 263 | case _monthCode: 264 | month = number; 265 | break; 266 | case _dayCode: 267 | day = number; 268 | break; 269 | case _hourCode: 270 | hour = number; 271 | break; 272 | case _minuteCode: 273 | minute = number; 274 | break; 275 | case _secondCode: 276 | second = number; 277 | break; 278 | case _fractionSecondCode: 279 | microsecond = number; 280 | break; 281 | } 282 | } else { 283 | return null; 284 | } 285 | } 286 | if (isUtc) { 287 | return DateTime.utc( 288 | year, 289 | month, 290 | day, 291 | hour, 292 | minute, 293 | second, 294 | 0, 295 | microsecond, 296 | ); 297 | } else { 298 | return DateTime( 299 | year, 300 | month, 301 | day, 302 | hour, 303 | minute, 304 | second, 305 | 0, 306 | microsecond, 307 | ); 308 | } 309 | } 310 | 311 | int? _extractNumberFromString( 312 | String formattedDateTime, 313 | int index, 314 | bool throwOnError, 315 | ) { 316 | var parsed = tryParse( 317 | formattedDateTime, 318 | _blocks.starts[index], 319 | _blocks.ends[index], 320 | ); 321 | if (parsed == null && throwOnError) { 322 | throw FormatException( 323 | 'Expected digits at ${formattedDateTime.substring( 324 | _blocks.starts[index], 325 | _blocks.ends[index], 326 | )}', 327 | formattedDateTime, 328 | _blocks.starts[index], 329 | ); 330 | } 331 | return parsed; 332 | } 333 | 334 | int? tryParse(String formattedDateTime, int start, int end) { 335 | var result = 0; 336 | for (var i = start; i < end; i++) { 337 | var digit = formattedDateTime.codeUnitAt(i) ^ 0x30; 338 | if (digit <= 9) { 339 | result = result * 10 + digit; 340 | } else { 341 | return null; 342 | } 343 | } 344 | return result; 345 | } 346 | } 347 | 348 | class _ParsedFormatBlocks { 349 | final formatCharacters = []; 350 | final starts = []; 351 | final ends = []; 352 | 353 | _ParsedFormatBlocks(); 354 | 355 | int get length => formatCharacters.length; 356 | 357 | void saveBlock(int? char, int start, int end) { 358 | if (char != null) { 359 | if (char == FixedDateTimeFormatter._fractionSecondCode && 360 | end - start > 6) { 361 | throw FormatException( 362 | 'Fractional seconds can only be specified up to microseconds', 363 | char, 364 | start, 365 | ); 366 | } else if (end - start > 9) { 367 | throw FormatException( 368 | 'Length of a format char block cannot be larger than 9', 369 | char, 370 | start, 371 | ); 372 | } 373 | formatCharacters.add(char); 374 | starts.add(start); 375 | ends.add(end); 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /lib/src/hex.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'hex/decoder.dart'; 8 | import 'hex/encoder.dart'; 9 | 10 | export 'hex/decoder.dart' hide hexDecoder; 11 | export 'hex/encoder.dart' hide hexEncoder; 12 | 13 | /// The canonical instance of [HexCodec]. 14 | const hex = HexCodec._(); 15 | 16 | /// A codec that converts byte arrays to and from hexadecimal strings, following 17 | /// [the Base16 spec](https://tools.ietf.org/html/rfc4648#section-8). 18 | /// 19 | /// This should be used via the [hex] field. 20 | class HexCodec extends Codec, String> { 21 | @override 22 | HexEncoder get encoder => hexEncoder; 23 | @override 24 | HexDecoder get decoder => hexDecoder; 25 | 26 | const HexCodec._(); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/hex/decoder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:typed_data'; 7 | 8 | import '../utils.dart'; 9 | 10 | /// The canonical instance of [HexDecoder]. 11 | const hexDecoder = HexDecoder._(); 12 | 13 | /// A converter that decodes hexadecimal strings into byte arrays. 14 | /// 15 | /// Because two hexadecimal digits correspond to a single byte, this will throw 16 | /// a [FormatException] if given an odd-length string. It will also throw a 17 | /// [FormatException] if given a string containing non-hexadecimal code units. 18 | class HexDecoder extends Converter> { 19 | const HexDecoder._(); 20 | 21 | @override 22 | Uint8List convert(String input) { 23 | if (!input.length.isEven) { 24 | throw FormatException( 25 | 'Invalid input length, must be even.', 26 | input, 27 | input.length, 28 | ); 29 | } 30 | 31 | var bytes = Uint8List(input.length ~/ 2); 32 | _decode(input.codeUnits, 0, input.length, bytes, 0); 33 | return bytes; 34 | } 35 | 36 | @override 37 | StringConversionSink startChunkedConversion(Sink> sink) => 38 | _HexDecoderSink(sink); 39 | } 40 | 41 | /// A conversion sink for chunked hexadecimal decoding. 42 | class _HexDecoderSink extends StringConversionSinkBase { 43 | /// The underlying sink to which decoded byte arrays will be passed. 44 | final Sink> _sink; 45 | 46 | /// The trailing digit from the previous string. 47 | /// 48 | /// This will be non-`null` if the most recent string had an odd number of 49 | /// hexadecimal digits. Since it's the most significant digit, it's always a 50 | /// multiple of 16. 51 | int? _lastDigit; 52 | 53 | _HexDecoderSink(this._sink); 54 | 55 | @override 56 | void addSlice(String string, int start, int end, bool isLast) { 57 | RangeError.checkValidRange(start, end, string.length); 58 | 59 | if (start == end) { 60 | if (isLast) _close(string, end); 61 | return; 62 | } 63 | 64 | var codeUnits = string.codeUnits; 65 | Uint8List bytes; 66 | int bytesStart; 67 | if (_lastDigit == null) { 68 | bytes = Uint8List((end - start) ~/ 2); 69 | bytesStart = 0; 70 | } else { 71 | var hexPairs = (end - start - 1) ~/ 2; 72 | bytes = Uint8List(1 + hexPairs); 73 | bytes[0] = _lastDigit! + digitForCodeUnit(codeUnits, start); 74 | start++; 75 | bytesStart = 1; 76 | } 77 | 78 | _lastDigit = _decode(codeUnits, start, end, bytes, bytesStart); 79 | 80 | _sink.add(bytes); 81 | if (isLast) _close(string, end); 82 | } 83 | 84 | @override 85 | ByteConversionSink asUtf8Sink(bool allowMalformed) => 86 | _HexDecoderByteSink(_sink); 87 | 88 | @override 89 | void close() => _close(); 90 | 91 | /// Like [close], but includes [string] and [index] in the [FormatException] 92 | /// if one is thrown. 93 | void _close([String? string, int? index]) { 94 | if (_lastDigit != null) { 95 | throw FormatException( 96 | 'Input ended with incomplete encoded byte.', string, index); 97 | } 98 | 99 | _sink.close(); 100 | } 101 | } 102 | 103 | /// A conversion sink for chunked hexadecimal decoding from UTF-8 bytes. 104 | class _HexDecoderByteSink extends ByteConversionSinkBase { 105 | /// The underlying sink to which decoded byte arrays will be passed. 106 | final Sink> _sink; 107 | 108 | /// The trailing digit from the previous string. 109 | /// 110 | /// This will be non-`null` if the most recent string had an odd number of 111 | /// hexadecimal digits. Since it's the most significant digit, it's always a 112 | /// multiple of 16. 113 | int? _lastDigit; 114 | 115 | _HexDecoderByteSink(this._sink); 116 | 117 | @override 118 | void add(List chunk) => addSlice(chunk, 0, chunk.length, false); 119 | 120 | @override 121 | void addSlice(List chunk, int start, int end, bool isLast) { 122 | RangeError.checkValidRange(start, end, chunk.length); 123 | 124 | if (start == end) { 125 | if (isLast) _close(chunk, end); 126 | return; 127 | } 128 | 129 | Uint8List bytes; 130 | int bytesStart; 131 | if (_lastDigit == null) { 132 | bytes = Uint8List((end - start) ~/ 2); 133 | bytesStart = 0; 134 | } else { 135 | var hexPairs = (end - start - 1) ~/ 2; 136 | bytes = Uint8List(1 + hexPairs); 137 | bytes[0] = _lastDigit! + digitForCodeUnit(chunk, start); 138 | start++; 139 | bytesStart = 1; 140 | } 141 | 142 | _lastDigit = _decode(chunk, start, end, bytes, bytesStart); 143 | 144 | _sink.add(bytes); 145 | if (isLast) _close(chunk, end); 146 | } 147 | 148 | @override 149 | void close() => _close(); 150 | 151 | /// Like [close], but includes [chunk] and [index] in the [FormatException] 152 | /// if one is thrown. 153 | void _close([List? chunk, int? index]) { 154 | if (_lastDigit != null) { 155 | throw FormatException( 156 | 'Input ended with incomplete encoded byte.', chunk, index); 157 | } 158 | 159 | _sink.close(); 160 | } 161 | } 162 | 163 | /// Decodes [codeUnits] and writes the result into [destination]. 164 | /// 165 | /// This reads from [codeUnits] between [sourceStart] and [sourceEnd]. It writes 166 | /// the result into [destination] starting at [destinationStart]. 167 | /// 168 | /// If there's a leftover digit at the end of the decoding, this returns that 169 | /// digit. Otherwise it returns `null`. 170 | int? _decode(List codeUnits, int sourceStart, int sourceEnd, 171 | List destination, int destinationStart) { 172 | var destinationIndex = destinationStart; 173 | for (var i = sourceStart; i < sourceEnd - 1; i += 2) { 174 | var firstDigit = digitForCodeUnit(codeUnits, i); 175 | var secondDigit = digitForCodeUnit(codeUnits, i + 1); 176 | destination[destinationIndex++] = 16 * firstDigit + secondDigit; 177 | } 178 | 179 | if ((sourceEnd - sourceStart).isEven) return null; 180 | return 16 * digitForCodeUnit(codeUnits, sourceEnd - 1); 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/hex/encoder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:typed_data'; 7 | 8 | import '../charcodes.dart'; 9 | 10 | /// The canonical instance of [HexEncoder]. 11 | const hexEncoder = HexEncoder._(); 12 | 13 | /// A converter that encodes byte arrays into hexadecimal strings. 14 | /// 15 | /// This will throw a [RangeError] if the byte array has any digits that don't 16 | /// fit in the gamut of a byte. 17 | class HexEncoder extends Converter, String> { 18 | const HexEncoder._(); 19 | 20 | @override 21 | String convert(List input) => _convert(input, 0, input.length); 22 | 23 | @override 24 | ByteConversionSink startChunkedConversion(Sink sink) => 25 | _HexEncoderSink(sink); 26 | } 27 | 28 | /// A conversion sink for chunked hexadecimal encoding. 29 | class _HexEncoderSink extends ByteConversionSinkBase { 30 | /// The underlying sink to which decoded byte arrays will be passed. 31 | final Sink _sink; 32 | 33 | _HexEncoderSink(this._sink); 34 | 35 | @override 36 | void add(List chunk) { 37 | _sink.add(_convert(chunk, 0, chunk.length)); 38 | } 39 | 40 | @override 41 | void addSlice(List chunk, int start, int end, bool isLast) { 42 | RangeError.checkValidRange(start, end, chunk.length); 43 | _sink.add(_convert(chunk, start, end)); 44 | if (isLast) _sink.close(); 45 | } 46 | 47 | @override 48 | void close() { 49 | _sink.close(); 50 | } 51 | } 52 | 53 | String _convert(List bytes, int start, int end) { 54 | // A Uint8List is more efficient than a StringBuffer given that we know that 55 | // we're only emitting ASCII-compatible characters, and that we know the 56 | // length ahead of time. 57 | var buffer = Uint8List((end - start) * 2); 58 | var bufferIndex = 0; 59 | 60 | // A bitwise OR of all bytes in [bytes]. This allows us to check for 61 | // out-of-range bytes without adding more branches than necessary to the 62 | // core loop. 63 | var byteOr = 0; 64 | for (var i = start; i < end; i++) { 65 | var byte = bytes[i]; 66 | byteOr |= byte; 67 | 68 | // The bitwise arithmetic here is equivalent to `byte ~/ 16` and `byte % 16` 69 | // for valid byte values, but is easier for dart2js to optimize given that 70 | // it can't prove that [byte] will always be positive. 71 | buffer[bufferIndex++] = _codeUnitForDigit((byte & 0xF0) >> 4); 72 | buffer[bufferIndex++] = _codeUnitForDigit(byte & 0x0F); 73 | } 74 | 75 | if (byteOr >= 0 && byteOr <= 255) return String.fromCharCodes(buffer); 76 | 77 | // If there was an invalid byte, find it and throw an exception. 78 | for (var i = start; i < end; i++) { 79 | var byte = bytes[i]; 80 | if (byte >= 0 && byte <= 0xff) continue; 81 | throw FormatException( 82 | "Invalid byte ${byte < 0 ? "-" : ""}0x${byte.abs().toRadixString(16)}.", 83 | bytes, 84 | i); 85 | } 86 | 87 | throw StateError('unreachable'); 88 | } 89 | 90 | /// Returns the ASCII/Unicode code unit corresponding to the hexadecimal digit 91 | /// [digit]. 92 | int _codeUnitForDigit(int digit) => digit < 10 ? digit + $0 : digit + $a - 10; 93 | -------------------------------------------------------------------------------- /lib/src/identity_codec.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | class _IdentityConverter extends Converter { 8 | _IdentityConverter(); 9 | @override 10 | T convert(T input) => input; 11 | } 12 | 13 | /// A [Codec] that performs the identity conversion (changing nothing) in both 14 | /// directions. 15 | /// 16 | /// The identity codec passes input directly to output in both directions. 17 | /// This class can be used as a base when combining multiple codecs, 18 | /// because fusing the identity codec with any other codec gives the other 19 | /// codec back. 20 | /// 21 | /// Note, that when fused with another [Codec] the identity codec disppears. 22 | class IdentityCodec extends Codec { 23 | const IdentityCodec(); 24 | 25 | @override 26 | Converter get decoder => _IdentityConverter(); 27 | @override 28 | Converter get encoder => _IdentityConverter(); 29 | 30 | /// Fuse with an other codec. 31 | /// 32 | /// Fusing with the identify converter is a no-op, so this always return 33 | /// [other]. 34 | @override 35 | Codec fuse(Codec other) => other; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/percent.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'percent/decoder.dart'; 8 | import 'percent/encoder.dart'; 9 | 10 | export 'percent/decoder.dart' hide percentDecoder; 11 | export 'percent/encoder.dart' hide percentEncoder; 12 | 13 | /// The canonical instance of [PercentCodec]. 14 | const percent = PercentCodec._(); 15 | 16 | // TODO(nweiz): Add flags to support generating and interpreting "+" as a space 17 | // character. Also add an option for custom sets of unreserved characters. 18 | /// A codec that converts byte arrays to and from percent-encoded (also known as 19 | /// URL-encoded) strings according to 20 | /// [RFC 3986](https://tools.ietf.org/html/rfc3986#section-2.1). 21 | /// 22 | /// [encoder] encodes all bytes other than ASCII letters, decimal digits, or one 23 | /// of `-._~`. This matches the behavior of [Uri.encodeQueryComponent] except 24 | /// that it doesn't encode `0x20` bytes to the `+` character. 25 | /// 26 | /// To be maximally flexible, [decoder] will decode any percent-encoded byte and 27 | /// will allow any non-percent-encoded byte other than `%`. By default, it 28 | /// interprets `+` as `0x2B` rather than `0x20` as emitted by 29 | /// [Uri.encodeQueryComponent]. 30 | class PercentCodec extends Codec, String> { 31 | @override 32 | PercentEncoder get encoder => percentEncoder; 33 | @override 34 | PercentDecoder get decoder => percentDecoder; 35 | 36 | const PercentCodec._(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/percent/decoder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import 'package:typed_data/typed_data.dart'; 8 | 9 | import '../charcodes.dart'; 10 | import '../utils.dart'; 11 | 12 | /// The canonical instance of [PercentDecoder]. 13 | const percentDecoder = PercentDecoder._(); 14 | 15 | const _lastPercent = -1; 16 | 17 | /// A converter that decodes percent-encoded strings into byte arrays. 18 | /// 19 | /// To be maximally flexible, this will decode any percent-encoded byte and 20 | /// will allow any non-percent-encoded byte other than `%`. By default, it 21 | /// interprets `+` as `0x2B` rather than `0x20` as emitted by 22 | /// [Uri.encodeQueryComponent]. 23 | /// 24 | /// This will throw a [FormatException] if the input string has an incomplete 25 | /// percent-encoding, or if it contains non-ASCII code units. 26 | class PercentDecoder extends Converter> { 27 | const PercentDecoder._(); 28 | 29 | @override 30 | List convert(String input) { 31 | var buffer = Uint8Buffer(); 32 | var lastDigit = _decode(input.codeUnits, 0, input.length, buffer); 33 | 34 | if (lastDigit != null) { 35 | throw FormatException( 36 | 'Input ended with incomplete encoded byte.', input, input.length); 37 | } 38 | 39 | return buffer.buffer.asUint8List(0, buffer.length); 40 | } 41 | 42 | @override 43 | StringConversionSink startChunkedConversion(Sink> sink) => 44 | _PercentDecoderSink(sink); 45 | } 46 | 47 | /// A conversion sink for chunked percent-encoded decoding. 48 | class _PercentDecoderSink extends StringConversionSinkBase { 49 | /// The underlying sink to which decoded byte arrays will be passed. 50 | final Sink> _sink; 51 | 52 | /// The trailing digit from the previous string. 53 | /// 54 | /// This is `null` if the previous string ended with a complete 55 | /// percent-encoded byte or a literal character. It's [_lastPercent] if the 56 | /// most recent string ended with `%`. Otherwise, the most recent string ended 57 | /// with a `%` followed by a hexadecimal digit, and this is that digit. Since 58 | /// it's the most significant digit, it's always a multiple of 16. 59 | int? _lastDigit; 60 | 61 | _PercentDecoderSink(this._sink); 62 | 63 | @override 64 | void addSlice(String string, int start, int end, bool isLast) { 65 | RangeError.checkValidRange(start, end, string.length); 66 | 67 | if (start == end) { 68 | if (isLast) _close(string, end); 69 | return; 70 | } 71 | 72 | var buffer = Uint8Buffer(); 73 | var codeUnits = string.codeUnits; 74 | if (_lastDigit == _lastPercent) { 75 | _lastDigit = 16 * digitForCodeUnit(codeUnits, start); 76 | start++; 77 | 78 | if (start == end) { 79 | if (isLast) _close(string, end); 80 | return; 81 | } 82 | } 83 | 84 | if (_lastDigit != null) { 85 | buffer.add(_lastDigit! + digitForCodeUnit(codeUnits, start)); 86 | start++; 87 | } 88 | 89 | _lastDigit = _decode(codeUnits, start, end, buffer); 90 | 91 | _sink.add(buffer.buffer.asUint8List(0, buffer.length)); 92 | if (isLast) _close(string, end); 93 | } 94 | 95 | @override 96 | ByteConversionSink asUtf8Sink(bool allowMalformed) => 97 | _PercentDecoderByteSink(_sink); 98 | 99 | @override 100 | void close() => _close(); 101 | 102 | /// Like [close], but includes [string] and [index] in the [FormatException] 103 | /// if one is thrown. 104 | void _close([String? string, int? index]) { 105 | if (_lastDigit != null) { 106 | throw FormatException( 107 | 'Input ended with incomplete encoded byte.', string, index); 108 | } 109 | 110 | _sink.close(); 111 | } 112 | } 113 | 114 | /// A conversion sink for chunked percent-encoded decoding from UTF-8 bytes. 115 | class _PercentDecoderByteSink extends ByteConversionSinkBase { 116 | /// The underlying sink to which decoded byte arrays will be passed. 117 | final Sink> _sink; 118 | 119 | /// The trailing digit from the previous string. 120 | /// 121 | /// This is `null` if the previous string ended with a complete 122 | /// percent-encoded byte or a literal character. It's [_lastPercent] if the 123 | /// most recent string ended with `%`. Otherwise, the most recent string ended 124 | /// with a `%` followed by a hexadecimal digit, and this is that digit. Since 125 | /// it's the most significant digit, it's always a multiple of 16. 126 | int? _lastDigit; 127 | 128 | _PercentDecoderByteSink(this._sink); 129 | 130 | @override 131 | void add(List chunk) => addSlice(chunk, 0, chunk.length, false); 132 | 133 | @override 134 | void addSlice(List chunk, int start, int end, bool isLast) { 135 | RangeError.checkValidRange(start, end, chunk.length); 136 | 137 | if (start == end) { 138 | if (isLast) _close(chunk, end); 139 | return; 140 | } 141 | 142 | var buffer = Uint8Buffer(); 143 | if (_lastDigit == _lastPercent) { 144 | _lastDigit = 16 * digitForCodeUnit(chunk, start); 145 | start++; 146 | 147 | if (start == end) { 148 | if (isLast) _close(chunk, end); 149 | return; 150 | } 151 | } 152 | 153 | if (_lastDigit != null) { 154 | buffer.add(_lastDigit! + digitForCodeUnit(chunk, start)); 155 | start++; 156 | } 157 | 158 | _lastDigit = _decode(chunk, start, end, buffer); 159 | 160 | _sink.add(buffer.buffer.asUint8List(0, buffer.length)); 161 | if (isLast) _close(chunk, end); 162 | } 163 | 164 | @override 165 | void close() => _close(); 166 | 167 | /// Like [close], but includes [chunk] and [index] in the [FormatException] 168 | /// if one is thrown. 169 | void _close([List? chunk, int? index]) { 170 | if (_lastDigit != null) { 171 | throw FormatException( 172 | 'Input ended with incomplete encoded byte.', chunk, index); 173 | } 174 | 175 | _sink.close(); 176 | } 177 | } 178 | 179 | /// Decodes [codeUnits] and writes the result into [buffer]. 180 | /// 181 | /// This reads from [codeUnits] between [start] and [end]. It writes 182 | /// the result into [buffer] starting at [end]. 183 | /// 184 | /// If there's a leftover digit at the end of the decoding, this returns that 185 | /// digit. Otherwise it returns `null`. 186 | int? _decode(List codeUnits, int start, int end, Uint8Buffer buffer) { 187 | // A bitwise OR of all code units in [codeUnits]. This allows us to check for 188 | // out-of-range code units without adding more branches than necessary to the 189 | // core loop. 190 | var codeUnitOr = 0; 191 | 192 | // The beginning of the current slice of adjacent non-% characters. We can add 193 | // all of these to the buffer at once. 194 | var sliceStart = start; 195 | for (var i = start; i < end; i++) { 196 | // First, loop through non-% characters. 197 | var codeUnit = codeUnits[i]; 198 | if (codeUnits[i] != $percent) { 199 | codeUnitOr |= codeUnit; 200 | continue; 201 | } 202 | 203 | // We found a %. The slice from `sliceStart` to `i` represents characters 204 | // than can be copied to the buffer as-is. 205 | if (i > sliceStart) { 206 | _checkForInvalidCodeUnit(codeUnitOr, codeUnits, sliceStart, i); 207 | buffer.addAll(codeUnits, sliceStart, i); 208 | } 209 | 210 | // Now decode the percent-encoded byte and add it as well. 211 | i++; 212 | if (i >= end) return _lastPercent; 213 | 214 | var firstDigit = digitForCodeUnit(codeUnits, i); 215 | i++; 216 | if (i >= end) return 16 * firstDigit; 217 | 218 | var secondDigit = digitForCodeUnit(codeUnits, i); 219 | buffer.add(16 * firstDigit + secondDigit); 220 | 221 | // The next iteration will look for non-% characters again. 222 | sliceStart = i + 1; 223 | } 224 | 225 | if (end > sliceStart) { 226 | _checkForInvalidCodeUnit(codeUnitOr, codeUnits, sliceStart, end); 227 | if (start == sliceStart) { 228 | buffer.addAll(codeUnits); 229 | } else { 230 | buffer.addAll(codeUnits, sliceStart, end); 231 | } 232 | } 233 | 234 | return null; 235 | } 236 | 237 | void _checkForInvalidCodeUnit( 238 | int codeUnitOr, List codeUnits, int start, int end) { 239 | if (codeUnitOr >= 0 && codeUnitOr <= 0x7f) return; 240 | 241 | for (var i = start; i < end; i++) { 242 | var codeUnit = codeUnits[i]; 243 | if (codeUnit >= 0 && codeUnit <= 0x7f) continue; 244 | throw FormatException( 245 | 'Non-ASCII code unit ' 246 | "U+${codeUnit.toRadixString(16).padLeft(4, '0')}", 247 | codeUnits, 248 | i); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /lib/src/percent/encoder.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | import '../charcodes.dart'; 8 | 9 | /// The canonical instance of [PercentEncoder]. 10 | const percentEncoder = PercentEncoder._(); 11 | 12 | /// A converter that encodes byte arrays into percent-encoded strings. 13 | /// 14 | /// Encodes all bytes other than ASCII letters, decimal digits, or one 15 | /// of `-._~`. This matches the behavior of [Uri.encodeQueryComponent] except 16 | /// that it doesn't encode `0x20` bytes to the `+` character. 17 | /// 18 | /// This will throw a [RangeError] if the byte array has any digits that don't 19 | /// fit in the gamut of a byte. 20 | class PercentEncoder extends Converter, String> { 21 | const PercentEncoder._(); 22 | 23 | @override 24 | String convert(List input) => _convert(input, 0, input.length); 25 | 26 | @override 27 | ByteConversionSink startChunkedConversion(Sink sink) => 28 | _PercentEncoderSink(sink); 29 | } 30 | 31 | /// A conversion sink for chunked percentadecimal encoding. 32 | class _PercentEncoderSink extends ByteConversionSinkBase { 33 | /// The underlying sink to which decoded byte arrays will be passed. 34 | final Sink _sink; 35 | 36 | _PercentEncoderSink(this._sink); 37 | 38 | @override 39 | void add(List chunk) { 40 | _sink.add(_convert(chunk, 0, chunk.length)); 41 | } 42 | 43 | @override 44 | void addSlice(List chunk, int start, int end, bool isLast) { 45 | RangeError.checkValidRange(start, end, chunk.length); 46 | _sink.add(_convert(chunk, start, end)); 47 | if (isLast) _sink.close(); 48 | } 49 | 50 | @override 51 | void close() { 52 | _sink.close(); 53 | } 54 | } 55 | 56 | String _convert(List bytes, int start, int end) { 57 | var buffer = StringBuffer(); 58 | 59 | // A bitwise OR of all bytes in [bytes]. This allows us to check for 60 | // out-of-range bytes without adding more branches than necessary to the 61 | // core loop. 62 | var byteOr = 0; 63 | for (var i = start; i < end; i++) { 64 | var byte = bytes[i]; 65 | byteOr |= byte; 66 | 67 | // If the byte is an uppercase letter, convert it to lowercase to check if 68 | // it's unreserved. This works because uppercase letters in ASCII are 69 | // exactly `0b100000 = 0x20` less than lowercase letters, so if we ensure 70 | // that that bit is 1 we ensure that the letter is lowercase. 71 | var letter = 0x20 | byte; 72 | if ((letter >= $a && letter <= $z) || 73 | (byte >= $0 && byte <= $9) || 74 | byte == $dash || 75 | byte == $dot || 76 | byte == $underscore || 77 | byte == $tilde) { 78 | // Unreserved characters are safe to write as-is. 79 | buffer.writeCharCode(byte); 80 | continue; 81 | } 82 | 83 | buffer.writeCharCode($percent); 84 | 85 | // The bitwise arithmetic here is equivalent to `byte ~/ 16` and `byte % 16` 86 | // for valid byte values, but is easier for dart2js to optimize given that 87 | // it can't prove that [byte] will always be positive. 88 | buffer.writeCharCode(_codeUnitForDigit((byte & 0xF0) >> 4)); 89 | buffer.writeCharCode(_codeUnitForDigit(byte & 0x0F)); 90 | } 91 | 92 | if (byteOr >= 0 && byteOr <= 255) return buffer.toString(); 93 | 94 | // If there was an invalid byte, find it and throw an exception. 95 | for (var i = start; i < end; i++) { 96 | var byte = bytes[i]; 97 | if (byte >= 0 && byte <= 0xff) continue; 98 | throw FormatException( 99 | "Invalid byte ${byte < 0 ? "-" : ""}0x${byte.abs().toRadixString(16)}.", 100 | bytes, 101 | i); 102 | } 103 | 104 | throw StateError('unreachable'); 105 | } 106 | 107 | /// Returns the ASCII/Unicode code unit corresponding to the hexadecimal digit 108 | /// [digit]. 109 | int _codeUnitForDigit(int digit) => digit < 10 ? digit + $0 : digit + $A - 10; 110 | -------------------------------------------------------------------------------- /lib/src/string_accumulator_sink.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | 7 | /// A sink that provides access to the concatenated strings passed to it. 8 | /// 9 | /// See also [StringConversionSink.withCallback]. 10 | class StringAccumulatorSink extends StringConversionSinkBase { 11 | /// The string accumulated so far. 12 | String get string => _buffer.toString(); 13 | final _buffer = StringBuffer(); 14 | 15 | /// Whether [close] has been called. 16 | bool get isClosed => _isClosed; 17 | var _isClosed = false; 18 | 19 | /// Empties [string]. 20 | /// 21 | /// This can be used to avoid double-processing data. 22 | void clear() { 23 | _buffer.clear(); 24 | } 25 | 26 | @override 27 | void add(String str) { 28 | if (_isClosed) { 29 | throw StateError("Can't add to a closed sink."); 30 | } 31 | 32 | _buffer.write(str); 33 | } 34 | 35 | @override 36 | void addSlice(String chunk, int start, int end, bool isLast) { 37 | if (_isClosed) { 38 | throw StateError("Can't add to a closed sink."); 39 | } 40 | 41 | _buffer.write(chunk.substring(start, end)); 42 | if (isLast) _isClosed = true; 43 | } 44 | 45 | @override 46 | void close() { 47 | _isClosed = true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'charcodes.dart'; 6 | 7 | /// Returns the digit (0 through 15) corresponding to the hexadecimal code unit 8 | /// at index [index] in [codeUnits]. 9 | /// 10 | /// If the given code unit isn't valid hexadecimal, throws a [FormatException]. 11 | int digitForCodeUnit(List codeUnits, int index) { 12 | // If the code unit is a numeral, get its value. XOR works because 0 in ASCII 13 | // is `0b110000` and the other numerals come after it in ascending order and 14 | // take up at most four bits. 15 | // 16 | // We check for digits first because it ensures there's only a single branch 17 | // for 10 out of 16 of the expected cases. We don't count the `digit >= 0` 18 | // check because branch prediction will always work on it for valid data. 19 | var codeUnit = codeUnits[index]; 20 | var digit = $0 ^ codeUnit; 21 | if (digit <= 9) { 22 | if (digit >= 0) return digit; 23 | } else { 24 | // If the code unit is an uppercase letter, convert it to lowercase. This 25 | // works because uppercase letters in ASCII are exactly `0b100000 = 0x20` 26 | // less than lowercase letters, so if we ensure that that bit is 1 we ensure 27 | // that the letter is lowercase. 28 | var letter = 0x20 | codeUnit; 29 | if ($a <= letter && letter <= $f) return letter - $a + 10; 30 | } 31 | 32 | throw FormatException( 33 | 'Invalid hexadecimal code unit ' 34 | "U+${codeUnit.toRadixString(16).padLeft(4, '0')}.", 35 | codeUnits, 36 | index); 37 | } 38 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: convert 2 | version: 3.1.2-wip 3 | description: >- 4 | Utilities for converting between data representations. 5 | Provides a number of Sink, Codec, Decoder, and Encoder types. 6 | repository: https://github.com/dart-lang/convert 7 | 8 | environment: 9 | sdk: ^3.4.0 10 | 11 | dependencies: 12 | typed_data: ^1.3.0 13 | 14 | dev_dependencies: 15 | benchmark_harness: ^2.2.0 16 | dart_flutter_team_lints: ^3.0.0 17 | test: ^1.17.0 18 | -------------------------------------------------------------------------------- /test/accumulator_sink_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:convert/convert.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | late AccumulatorSink sink; 10 | setUp(() { 11 | sink = AccumulatorSink(); 12 | }); 13 | 14 | test("provides access to events as they're added", () { 15 | expect(sink.events, isEmpty); 16 | 17 | sink.add(1); 18 | expect(sink.events, equals([1])); 19 | 20 | sink.add(2); 21 | expect(sink.events, equals([1, 2])); 22 | 23 | sink.add(3); 24 | expect(sink.events, equals([1, 2, 3])); 25 | }); 26 | 27 | test('clear() clears the events', () { 28 | sink 29 | ..add(1) 30 | ..add(2) 31 | ..add(3); 32 | expect(sink.events, equals([1, 2, 3])); 33 | 34 | sink.clear(); 35 | expect(sink.events, isEmpty); 36 | 37 | sink 38 | ..add(4) 39 | ..add(5) 40 | ..add(6); 41 | expect(sink.events, equals([4, 5, 6])); 42 | }); 43 | 44 | test('indicates whether the sink is closed', () { 45 | expect(sink.isClosed, isFalse); 46 | sink.close(); 47 | expect(sink.isClosed, isTrue); 48 | }); 49 | 50 | test("doesn't allow add() to be called after close()", () { 51 | sink.close(); 52 | expect(() => sink.add(1), throwsStateError); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/byte_accumulator_sink_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:convert/convert.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | late ByteAccumulatorSink sink; 10 | setUp(() { 11 | sink = ByteAccumulatorSink(); 12 | }); 13 | 14 | test('provides access to the concatenated bytes', () { 15 | expect(sink.bytes, isEmpty); 16 | 17 | sink.add([1, 2, 3]); 18 | expect(sink.bytes, equals([1, 2, 3])); 19 | 20 | sink.addSlice([4, 5, 6, 7, 8], 1, 4, false); 21 | expect(sink.bytes, equals([1, 2, 3, 5, 6, 7])); 22 | }); 23 | 24 | test('clear() clears the bytes', () { 25 | sink.add([1, 2, 3]); 26 | expect(sink.bytes, equals([1, 2, 3])); 27 | 28 | sink.clear(); 29 | expect(sink.bytes, isEmpty); 30 | 31 | sink.add([4, 5, 6]); 32 | expect(sink.bytes, equals([4, 5, 6])); 33 | }); 34 | 35 | test('indicates whether the sink is closed', () { 36 | expect(sink.isClosed, isFalse); 37 | sink.close(); 38 | expect(sink.isClosed, isTrue); 39 | }); 40 | 41 | test('indicates whether the sink is closed via addSlice', () { 42 | expect(sink.isClosed, isFalse); 43 | sink.addSlice([], 0, 0, true); 44 | expect(sink.isClosed, isTrue); 45 | }); 46 | 47 | test("doesn't allow add() to be called after close()", () { 48 | sink.close(); 49 | expect(() => sink.add([1]), throwsStateError); 50 | }); 51 | 52 | test("doesn't allow addSlice() to be called after close()", () { 53 | sink.close(); 54 | expect(() => sink.addSlice([], 0, 0, false), throwsStateError); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/codepage_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:core'; 7 | import 'dart:typed_data'; 8 | 9 | import 'package:convert/convert.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | var bytes = Uint8List.fromList([for (var i = 0; i < 256; i++) i]); 14 | for (var cp in [ 15 | latin2, 16 | latin3, 17 | latin4, 18 | latin5, 19 | latin6, 20 | latin7, 21 | latin8, 22 | latin9, 23 | latin10, 24 | latinCyrillic, 25 | latinGreek, 26 | latinHebrew, 27 | latinThai, 28 | latinArabic 29 | ]) { 30 | group('${cp.name} codepage', () { 31 | test('ascii compatible', () { 32 | for (var byte = 0x20; byte < 0x7f; byte++) { 33 | expect(cp[byte], byte); 34 | } 35 | }); 36 | 37 | test('bidirectional mapping', () { 38 | // Maps both directions. 39 | for (var byte = 0; byte < 256; byte++) { 40 | var char = cp[byte]; 41 | if (char != 0xFFFD) { 42 | var string = String.fromCharCode(char); 43 | expect(cp.encode(string), [byte]); 44 | expect(cp.decode([byte]), string); 45 | } 46 | } 47 | }); 48 | 49 | test('decode invalid characters not allowed', () { 50 | expect(() => cp.decode([0xfffd]), throwsA(isA())); 51 | }); 52 | 53 | test('decode invalid characters allowed', () { 54 | // Decode works like operator[]. 55 | expect(cp.decode(bytes, allowInvalid: true), 56 | String.fromCharCodes([for (var i = 0; i < 256; i++) cp[i]])); 57 | }); 58 | 59 | test('chunked conversion', () { 60 | late final String decodedString; 61 | final outputSink = StringConversionSink.withCallback( 62 | (accumulated) => decodedString = accumulated); 63 | final inputSink = cp.decoder.startChunkedConversion(outputSink); 64 | final expected = StringBuffer(); 65 | 66 | for (var byte = 0; byte < 256; byte++) { 67 | var char = cp[byte]; 68 | if (char != 0xFFFD) { 69 | inputSink.add([byte]); 70 | expected.writeCharCode(char); 71 | } 72 | } 73 | inputSink.close(); 74 | expect(decodedString, expected.toString()); 75 | }); 76 | }); 77 | } 78 | test('latin-2 roundtrip', () { 79 | // Data from http://www.columbia.edu/kermit/latin2.html 80 | var latin2text = '\xa0Ą˘Ł¤ĽŚ§¨ŠŞŤŹ\xadŽŻ°ą˛ł´ľśˇ¸šşťź˝žżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇ' 81 | 'ÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ˙'; 82 | expect(latin2.decode(latin2.encode(latin2text)), latin2text); 83 | }); 84 | 85 | test('latin-3 roundtrip', () { 86 | // Data from http://www.columbia.edu/kermit/latin3.html 87 | var latin2text = '\xa0Ħ˘£¤\u{FFFD}Ĥ§¨İŞĞĴ\xad\u{FFFD}ݰħ²³´µĥ·¸ışğĵ½' 88 | '\u{FFFD}żÀÁÂ\u{FFFD}ÄĊĈÇÈÉÊËÌÍÎÏ\u{FFFD}ÑÒÓÔĠÖ×ĜÙÚÛÜŬŜßàáâ' 89 | '\u{FFFD}äċĉçèéêëìíîï\u{FFFD}ñòóôġö÷ĝùúûüŭŝ˙'; 90 | var encoded = latin3.encode(latin2text, invalidCharacter: 0); 91 | var decoded = latin3.decode(encoded, allowInvalid: true); 92 | expect(decoded, latin2text); 93 | }); 94 | 95 | group('Custom code page', () { 96 | late final cp = CodePage('custom', "ABCDEF${"\uFFFD" * 250}"); 97 | 98 | test('simple encode', () { 99 | var result = cp.encode('BADCAFE'); 100 | expect(result, [1, 0, 3, 2, 0, 5, 4]); 101 | }); 102 | 103 | test('unencodable character', () { 104 | expect(() => cp.encode('GAD'), throwsFormatException); 105 | }); 106 | 107 | test('unencodable character with invalidCharacter', () { 108 | expect(cp.encode('GAD', invalidCharacter: 0x3F), [0x3F, 0, 3]); 109 | }); 110 | 111 | test('simple decode', () { 112 | expect(cp.decode([1, 0, 3, 2, 0, 5, 4]), 'BADCAFE'); 113 | }); 114 | 115 | test('undecodable byte', () { 116 | expect(() => cp.decode([6, 1, 255]), throwsFormatException); 117 | }); 118 | 119 | test('undecodable byte with allowInvalid', () { 120 | expect(cp.decode([6, 1, 255], allowInvalid: true), '\u{FFFD}B\u{FFFD}'); 121 | }); 122 | 123 | test('chunked conversion', () { 124 | late final String decodedString; 125 | final outputSink = StringConversionSink.withCallback( 126 | (accumulated) => decodedString = accumulated); 127 | final inputSink = cp.decoder.startChunkedConversion(outputSink); 128 | 129 | inputSink 130 | ..add([1]) 131 | ..add([0]) 132 | ..add([3]) 133 | ..close(); 134 | expect(decodedString, 'BAD'); 135 | }); 136 | 137 | test('chunked conversion - byte conversion sink', () { 138 | late final String decodedString; 139 | final outputSink = StringConversionSink.withCallback( 140 | (accumulated) => decodedString = accumulated); 141 | final bytes = [1, 0, 3, 2, 0, 5, 4]; 142 | 143 | final inputSink = cp.decoder.startChunkedConversion(outputSink); 144 | expect(inputSink, isA()); 145 | 146 | (inputSink as ByteConversionSink) 147 | ..addSlice(bytes, 1, 3, false) 148 | ..addSlice(bytes, 4, 5, false) 149 | ..addSlice(bytes, 6, 6, true); 150 | 151 | expect(decodedString, 'ADA'); 152 | }); 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /test/fixed_datetime_formatter_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:convert/src/fixed_datetime_formatter.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | var noFractionalSeconds = DateTime.utc(0); 10 | var skipWeb = { 11 | 'js': const Skip( 12 | 'Web does not support microseconds (see https://github.com/dart-lang/sdk/issues/44876)') 13 | }; 14 | // Testing `decode`. 15 | test('Parse only year', () { 16 | var time = FixedDateTimeFormatter('YYYY').decode('1996'); 17 | expect(time, DateTime.utc(1996)); 18 | }); 19 | test('Escaped chars are ignored', () { 20 | var time = FixedDateTimeFormatter('YYYY kiwi MM').decode('1996 rnad 01'); 21 | expect(time, DateTime.utc(1996)); 22 | }); 23 | test('Parse two years throws', () { 24 | expect(() => FixedDateTimeFormatter('YYYY YYYY'), throwsException); 25 | }); 26 | test('Parse year and century', () { 27 | var time = FixedDateTimeFormatter('CCYY').decode('1996'); 28 | expect(time, DateTime.utc(1996)); 29 | }); 30 | test('Parse year, decade and century', () { 31 | var time = FixedDateTimeFormatter('CCEY').decode('1996'); 32 | expect(time, DateTime.utc(1996)); 33 | }); 34 | test('Parse year, century, month', () { 35 | var time = FixedDateTimeFormatter('CCYY MM').decode('1996 04'); 36 | expect(time, DateTime.utc(1996, 4)); 37 | }); 38 | test('Parse year, century, month, day', () { 39 | var time = FixedDateTimeFormatter('CCYY MM-DD').decode('1996 04-25'); 40 | expect(time, DateTime.utc(1996, 4, 25)); 41 | }); 42 | test('Parse year, century, month, day, hour, minute, second', () { 43 | var time = FixedDateTimeFormatter('CCYY MM-DD hh:mm:ss') 44 | .decode('1996 04-25 05:03:22'); 45 | expect(time, DateTime.utc(1996, 4, 25, 5, 3, 22)); 46 | }); 47 | test('Parse YYYYMMDDhhmmssSSS', () { 48 | var time = 49 | FixedDateTimeFormatter('YYYYMMDDhhmmssSSS').decode('19960425050322533'); 50 | expect(time, DateTime.utc(1996, 4, 25, 5, 3, 22, 533)); 51 | }); 52 | test('Parse S 1/10 of a second', () { 53 | var time = FixedDateTimeFormatter('S').decode('1'); 54 | expect(time, noFractionalSeconds.add(const Duration(milliseconds: 100))); 55 | }); 56 | test('Parse SS 1/100 of a second', () { 57 | var time = FixedDateTimeFormatter('SS').decode('01'); 58 | expect(time, noFractionalSeconds.add(const Duration(milliseconds: 10))); 59 | }); 60 | test('Parse SSS a millisecond', () { 61 | var time = FixedDateTimeFormatter('SSS').decode('001'); 62 | expect(time, noFractionalSeconds.add(const Duration(milliseconds: 1))); 63 | }); 64 | test('Parse SSSSSS a microsecond', () { 65 | var time = FixedDateTimeFormatter('SSSSSS').decode('000001'); 66 | expect(time, noFractionalSeconds.add(const Duration(microseconds: 1))); 67 | }, onPlatform: skipWeb); 68 | test('Parse SSSSSS a millisecond', () { 69 | var time = FixedDateTimeFormatter('SSSSSS').decode('001000'); 70 | expect(time, noFractionalSeconds.add(const Duration(milliseconds: 1))); 71 | }); 72 | test('Parse SSSSSS a millisecond and a microsecond', () { 73 | var time = FixedDateTimeFormatter('SSSSSS').decode('001001'); 74 | expect( 75 | time, 76 | noFractionalSeconds.add(const Duration( 77 | milliseconds: 1, 78 | microseconds: 1, 79 | ))); 80 | }, onPlatform: skipWeb); 81 | test('Parse ssSSSSSS a second and a microsecond', () { 82 | var time = FixedDateTimeFormatter('ssSSSSSS').decode('01000001'); 83 | expect( 84 | time, 85 | noFractionalSeconds.add(const Duration( 86 | seconds: 1, 87 | microseconds: 1, 88 | ))); 89 | }, onPlatform: skipWeb); 90 | test('7 S throws', () { 91 | expect( 92 | () => FixedDateTimeFormatter('S' * 7), 93 | throwsFormatException, 94 | ); 95 | }); 96 | test('10 Y throws', () { 97 | expect( 98 | () => FixedDateTimeFormatter('Y' * 10), 99 | throwsFormatException, 100 | ); 101 | }); 102 | test('Parse hex year throws', () { 103 | expect( 104 | () => FixedDateTimeFormatter('YYYY').decode('0xAB'), 105 | throwsFormatException, 106 | ); 107 | }); 108 | // Testing `tryDecode`. 109 | test('Try parse year', () { 110 | var time = FixedDateTimeFormatter('YYYY').tryDecode('1996'); 111 | expect(time, DateTime.utc(1996)); 112 | }); 113 | test('Try parse hex year returns null', () { 114 | var time = FixedDateTimeFormatter('YYYY').tryDecode('0xAB'); 115 | expect(time, null); 116 | }); 117 | test('Try parse invalid returns null', () { 118 | var time = FixedDateTimeFormatter('YYYY').tryDecode('1x96'); 119 | expect(time, null); 120 | }); 121 | // Testing `encode`. 122 | test('Format simple', () { 123 | var time = DateTime.utc(1996); 124 | expect(FixedDateTimeFormatter('YYYY kiwi MM').encode(time), '1996 kiwi 01'); 125 | }); 126 | test('Format YYYYMMDDhhmmss', () { 127 | var time = DateTime.utc(1996, 4, 25, 5, 3, 22); 128 | expect( 129 | FixedDateTimeFormatter('YYYYMMDDhhmmss').encode(time), 130 | '19960425050322', 131 | ); 132 | }); 133 | test('Format CCEY-MM', () { 134 | var str = FixedDateTimeFormatter('CCEY-MM').encode(DateTime.utc(1996, 4)); 135 | expect(str, '1996-04'); 136 | }); 137 | test('Format XCCEY-MMX', () { 138 | var str = FixedDateTimeFormatter('XCCEY-MMX').encode(DateTime.utc(1996, 4)); 139 | expect(str, 'X1996-04X'); 140 | }); 141 | test('Format S 1/10 of a second', () { 142 | var str = FixedDateTimeFormatter('S') 143 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 100))); 144 | expect(str, '1'); 145 | }); 146 | test('Format SS 1/100 of a second', () { 147 | var str = FixedDateTimeFormatter('SS') 148 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 10))); 149 | expect(str, '01'); 150 | }); 151 | test('Format SSS 1/100 of a second', () { 152 | var str = FixedDateTimeFormatter('SSS') 153 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 10))); 154 | expect(str, '010'); 155 | }); 156 | test('Format SSSS no fractions', () { 157 | var str = FixedDateTimeFormatter('SSSS').encode(noFractionalSeconds); 158 | expect(str, '0000'); 159 | }); 160 | test('Format SSSSSS no fractions', () { 161 | var str = FixedDateTimeFormatter('SSSSSS').encode(noFractionalSeconds); 162 | expect(str, '000000'); 163 | }); 164 | test('Format SSSS 1/10 of a second', () { 165 | var str = FixedDateTimeFormatter('SSSS') 166 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 100))); 167 | expect(str, '1000'); 168 | }); 169 | test('Format SSSS 1/100 of a second', () { 170 | var str = FixedDateTimeFormatter('SSSS') 171 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 10))); 172 | expect(str, '0100'); 173 | }); 174 | test('Format SSSS a millisecond', () { 175 | var str = FixedDateTimeFormatter('SSSS') 176 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 1))); 177 | expect(str, '0010'); 178 | }); 179 | test('Format SSSSSS a microsecond', () { 180 | var str = FixedDateTimeFormatter('SSSSSS') 181 | .encode(DateTime.utc(0, 1, 1, 0, 0, 0, 0, 1)); 182 | expect(str, '000001'); 183 | }, onPlatform: skipWeb); 184 | test('Format SSSSSS a millisecond and a microsecond', () { 185 | var dateTime = noFractionalSeconds.add(const Duration( 186 | milliseconds: 1, 187 | microseconds: 1, 188 | )); 189 | var str = FixedDateTimeFormatter('SSSSSS').encode(dateTime); 190 | expect(str, '001001'); 191 | }, onPlatform: skipWeb); 192 | test('Format SSSSSS0 a microsecond', () { 193 | var str = FixedDateTimeFormatter('SSSSSS0') 194 | .encode(noFractionalSeconds.add(const Duration(microseconds: 1))); 195 | expect(str, '0000010'); 196 | }, onPlatform: skipWeb); 197 | test('Format SSSSSS0 1/10 of a second', () { 198 | var str = FixedDateTimeFormatter('SSSSSS0') 199 | .encode(noFractionalSeconds.add(const Duration(milliseconds: 100))); 200 | expect(str, '1000000'); 201 | }); 202 | test('Parse ssSSSSSS a second and a microsecond', () { 203 | var dateTime = noFractionalSeconds.add(const Duration( 204 | seconds: 1, 205 | microseconds: 1, 206 | )); 207 | var str = FixedDateTimeFormatter('ssSSSSSS').encode(dateTime); 208 | expect(str, '01000001'); 209 | }, onPlatform: skipWeb); 210 | test('Parse ssSSSSSS0 a second and a microsecond', () { 211 | var dateTime = noFractionalSeconds.add(const Duration( 212 | seconds: 1, 213 | microseconds: 1, 214 | )); 215 | var str = FixedDateTimeFormatter('ssSSSSSS0').encode(dateTime); 216 | expect(str, '010000010'); 217 | }, onPlatform: skipWeb); 218 | test('Parse negative year throws Error', () { 219 | expect( 220 | () => FixedDateTimeFormatter('YYYY').encode(DateTime(-1)), 221 | throwsArgumentError, 222 | ); 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /test/hex_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:convert/convert.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('encoder', () { 13 | test('converts byte arrays to hex', () { 14 | expect(hex.encode([0x1a, 0xb2, 0x3c, 0xd4]), equals('1ab23cd4')); 15 | expect(hex.encode([0x00, 0x01, 0xfe, 0xff]), equals('0001feff')); 16 | }); 17 | 18 | group('with chunked conversion', () { 19 | test('converts byte arrays to hex', () { 20 | var results = []; 21 | var controller = StreamController(sync: true); 22 | controller.stream.listen(results.add); 23 | var sink = hex.encoder.startChunkedConversion(controller.sink); 24 | 25 | sink.add([0x1a, 0xb2, 0x3c, 0xd4]); 26 | expect(results, equals(['1ab23cd4'])); 27 | 28 | sink.add([0x00, 0x01, 0xfe, 0xff]); 29 | expect(results, equals(['1ab23cd4', '0001feff'])); 30 | }); 31 | 32 | test('handles empty and single-byte lists', () { 33 | var results = []; 34 | var controller = StreamController(sync: true); 35 | controller.stream.listen(results.add); 36 | var sink = hex.encoder.startChunkedConversion(controller.sink); 37 | 38 | sink.add([]); 39 | expect(results, equals([''])); 40 | 41 | sink.add([0x00]); 42 | expect(results, equals(['', '00'])); 43 | 44 | sink.add([]); 45 | expect(results, equals(['', '00', ''])); 46 | }); 47 | }); 48 | 49 | test('rejects non-bytes', () { 50 | expect(() => hex.encode([0x100]), throwsFormatException); 51 | 52 | var sink = 53 | hex.encoder.startChunkedConversion(StreamController(sync: true)); 54 | expect(() => sink.add([0x100]), throwsFormatException); 55 | }); 56 | }); 57 | 58 | group('decoder', () { 59 | test('converts hex to byte arrays', () { 60 | expect(hex.decode('1ab23cd4'), equals([0x1a, 0xb2, 0x3c, 0xd4])); 61 | expect(hex.decode('0001feff'), equals([0x00, 0x01, 0xfe, 0xff])); 62 | }); 63 | 64 | test('supports uppercase letters', () { 65 | expect( 66 | hex.decode('0123456789ABCDEFabcdef'), 67 | equals([ 68 | 0x01, 69 | 0x23, 70 | 0x45, 71 | 0x67, 72 | 0x89, 73 | 0xab, 74 | 0xcd, 75 | 0xef, 76 | 0xab, 77 | 0xcd, 78 | 0xef 79 | ])); 80 | }); 81 | 82 | group('with chunked conversion', () { 83 | late List> results; 84 | late StringConversionSink sink; 85 | setUp(() { 86 | results = []; 87 | var controller = StreamController>(sync: true); 88 | controller.stream.listen(results.add); 89 | sink = hex.decoder.startChunkedConversion(controller.sink); 90 | }); 91 | 92 | test('converts hex to byte arrays', () { 93 | sink.add('1ab23cd4'); 94 | expect( 95 | results, 96 | equals([ 97 | [0x1a, 0xb2, 0x3c, 0xd4] 98 | ])); 99 | 100 | sink.add('0001feff'); 101 | expect( 102 | results, 103 | equals([ 104 | [0x1a, 0xb2, 0x3c, 0xd4], 105 | [0x00, 0x01, 0xfe, 0xff] 106 | ])); 107 | }); 108 | 109 | test('supports trailing digits split across chunks', () { 110 | sink.add('1ab23'); 111 | expect( 112 | results, 113 | equals([ 114 | [0x1a, 0xb2] 115 | ])); 116 | 117 | sink.add('cd'); 118 | expect( 119 | results, 120 | equals([ 121 | [0x1a, 0xb2], 122 | [0x3c] 123 | ])); 124 | 125 | sink.add('40001'); 126 | expect( 127 | results, 128 | equals([ 129 | [0x1a, 0xb2], 130 | [0x3c], 131 | [0xd4, 0x00, 0x01] 132 | ])); 133 | 134 | sink.add('feff'); 135 | expect( 136 | results, 137 | equals([ 138 | [0x1a, 0xb2], 139 | [0x3c], 140 | [0xd4, 0x00, 0x01], 141 | [0xfe, 0xff] 142 | ])); 143 | }); 144 | 145 | test('supports empty strings', () { 146 | sink.add(''); 147 | expect(results, isEmpty); 148 | 149 | sink.add('0'); 150 | expect(results, equals([[]])); 151 | 152 | sink.add(''); 153 | expect(results, equals([[]])); 154 | 155 | sink.add('0'); 156 | expect( 157 | results, 158 | equals([ 159 | [], 160 | [0x00] 161 | ])); 162 | 163 | sink.add(''); 164 | expect( 165 | results, 166 | equals([ 167 | [], 168 | [0x00] 169 | ])); 170 | }); 171 | 172 | test('rejects odd length detected in close()', () { 173 | sink.add('1ab23'); 174 | expect( 175 | results, 176 | equals([ 177 | [0x1a, 0xb2] 178 | ])); 179 | expect(() => sink.close(), throwsFormatException); 180 | }); 181 | 182 | test('rejects odd length detected in addSlice()', () { 183 | sink.addSlice('1ab23cd', 0, 5, false); 184 | expect( 185 | results, 186 | equals([ 187 | [0x1a, 0xb2] 188 | ])); 189 | 190 | expect( 191 | () => sink.addSlice('1ab23cd', 5, 7, true), throwsFormatException); 192 | }); 193 | }); 194 | 195 | group('rejects non-hex character', () { 196 | for (var char in [ 197 | 'g', 198 | 'G', 199 | '/', 200 | ':', 201 | '@', 202 | '`', 203 | '\x00', 204 | '\u0141', 205 | '\u{10041}' 206 | ]) { 207 | test('"$char"', () { 208 | expect(() => hex.decode('a$char'), throwsFormatException); 209 | expect(() => hex.decode('${char}a'), throwsFormatException); 210 | 211 | var sink = 212 | hex.decoder.startChunkedConversion(StreamController(sync: true)); 213 | expect(() => sink.add(char), throwsFormatException); 214 | }); 215 | } 216 | }); 217 | 218 | test('rejects odd length detected in convert()', () { 219 | expect(() => hex.decode('1ab23cd'), throwsFormatException); 220 | }); 221 | }); 222 | } 223 | -------------------------------------------------------------------------------- /test/identity_codec_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:convert/convert.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('IdentityCodec', () { 7 | test('encode', () { 8 | const codec = IdentityCodec(); 9 | expect(codec.encode('hello-world'), equals('hello-world')); 10 | }); 11 | 12 | test('decode', () { 13 | const codec = IdentityCodec(); 14 | expect(codec.decode('hello-world'), equals('hello-world')); 15 | }); 16 | 17 | test('fuse', () { 18 | const stringCodec = IdentityCodec(); 19 | final utf8Strings = stringCodec.fuse(utf8); 20 | expect(utf8Strings, equals(utf8)); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/percent_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:convert/convert.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void main() { 12 | group('encoder', () { 13 | test("doesn't percent-encode unreserved characters", () { 14 | var safeChars = 'abcdefghijklmnopqrstuvwxyz' 15 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 16 | '0123456789-._~'; 17 | expect(percent.encode([...safeChars.codeUnits]), equals(safeChars)); 18 | }); 19 | 20 | test('percent-encodes reserved ASCII characters', () { 21 | expect(percent.encode([...' `{@[,/^}\x7f\x00%'.codeUnits]), 22 | equals('%20%60%7B%40%5B%2C%2F%5E%7D%7F%00%25')); 23 | }); 24 | 25 | test('percent-encodes non-ASCII characters', () { 26 | expect(percent.encode([0x80, 0xFF]), equals('%80%FF')); 27 | }); 28 | 29 | test('mixes encoded and unencoded characters', () { 30 | expect(percent.encode([...'a+b=\x80'.codeUnits]), equals('a%2Bb%3D%80')); 31 | }); 32 | 33 | group('with chunked conversion', () { 34 | test('percent-encodes byte arrays', () { 35 | var results = []; 36 | var controller = StreamController(sync: true); 37 | controller.stream.listen(results.add); 38 | var sink = percent.encoder.startChunkedConversion(controller.sink); 39 | 40 | sink.add([...'a+b=\x80'.codeUnits]); 41 | expect(results, equals(['a%2Bb%3D%80'])); 42 | 43 | sink.add([0x00, 0x01, 0xfe, 0xff]); 44 | expect(results, equals(['a%2Bb%3D%80', '%00%01%FE%FF'])); 45 | }); 46 | 47 | test('handles empty and single-byte lists', () { 48 | var results = []; 49 | var controller = StreamController(sync: true); 50 | controller.stream.listen(results.add); 51 | var sink = percent.encoder.startChunkedConversion(controller.sink); 52 | 53 | sink.add([]); 54 | expect(results, equals([''])); 55 | 56 | sink.add([0x00]); 57 | expect(results, equals(['', '%00'])); 58 | 59 | sink.add([]); 60 | expect(results, equals(['', '%00', ''])); 61 | }); 62 | }); 63 | 64 | test('rejects non-bytes', () { 65 | expect(() => percent.encode([0x100]), throwsFormatException); 66 | 67 | var sink = 68 | percent.encoder.startChunkedConversion(StreamController(sync: true)); 69 | expect(() => sink.add([0x100]), throwsFormatException); 70 | }); 71 | }); 72 | 73 | group('decoder', () { 74 | test('converts percent-encoded strings to byte arrays', () { 75 | expect( 76 | percent.decode('a%2Bb%3D%801'), equals([...'a+b=\x801'.codeUnits])); 77 | }); 78 | 79 | test('supports lowercase letters', () { 80 | expect(percent.decode('a%2bb%3d%80'), equals([...'a+b=\x80'.codeUnits])); 81 | }); 82 | 83 | test('supports more aggressive encoding', () { 84 | expect(percent.decode('%61%2E%5A'), equals([...'a.Z'.codeUnits])); 85 | }); 86 | 87 | test('supports less aggressive encoding', () { 88 | var chars = ' `{@[,/^}\x7F\x00'; 89 | expect(percent.decode(chars), equals([...chars.codeUnits])); 90 | }); 91 | 92 | group('with chunked conversion', () { 93 | late List> results; 94 | late StringConversionSink sink; 95 | setUp(() { 96 | results = []; 97 | var controller = StreamController>(sync: true); 98 | controller.stream.listen(results.add); 99 | sink = percent.decoder.startChunkedConversion(controller.sink); 100 | }); 101 | 102 | test('converts percent to byte arrays', () { 103 | sink.add('a%2Bb%3D%801'); 104 | expect( 105 | results, 106 | equals([ 107 | [...'a+b=\x801'.codeUnits] 108 | ])); 109 | 110 | sink.add('%00%01%FE%FF'); 111 | expect( 112 | results, 113 | equals([ 114 | [...'a+b=\x801'.codeUnits], 115 | [0x00, 0x01, 0xfe, 0xff] 116 | ])); 117 | }); 118 | 119 | test('supports trailing percents and digits split across chunks', () { 120 | sink.add('ab%'); 121 | expect( 122 | results, 123 | equals([ 124 | [...'ab'.codeUnits] 125 | ])); 126 | 127 | sink.add('2'); 128 | expect( 129 | results, 130 | equals([ 131 | [...'ab'.codeUnits] 132 | ])); 133 | 134 | sink.add('0cd%2'); 135 | expect( 136 | results, 137 | equals([ 138 | [...'ab'.codeUnits], 139 | [...' cd'.codeUnits] 140 | ])); 141 | 142 | sink.add('0'); 143 | expect( 144 | results, 145 | equals([ 146 | [...'ab'.codeUnits], 147 | [...' cd'.codeUnits], 148 | [...' '.codeUnits] 149 | ])); 150 | }); 151 | 152 | test('supports empty strings', () { 153 | sink.add(''); 154 | expect(results, isEmpty); 155 | 156 | sink.add('%'); 157 | expect(results, equals([[]])); 158 | 159 | sink.add(''); 160 | expect(results, equals([[]])); 161 | 162 | sink.add('2'); 163 | expect(results, equals([[]])); 164 | 165 | sink.add(''); 166 | expect(results, equals([[]])); 167 | 168 | sink.add('0'); 169 | expect( 170 | results, 171 | equals([ 172 | [], 173 | [0x20] 174 | ])); 175 | }); 176 | 177 | test('rejects dangling % detected in close()', () { 178 | sink.add('ab%'); 179 | expect( 180 | results, 181 | equals([ 182 | [...'ab'.codeUnits] 183 | ])); 184 | expect(() => sink.close(), throwsFormatException); 185 | }); 186 | 187 | test('rejects dangling digit detected in close()', () { 188 | sink.add('ab%2'); 189 | expect( 190 | results, 191 | equals([ 192 | [...'ab'.codeUnits] 193 | ])); 194 | expect(() => sink.close(), throwsFormatException); 195 | }); 196 | 197 | test('rejects danging % detected in addSlice()', () { 198 | sink.addSlice('ab%', 0, 3, false); 199 | expect( 200 | results, 201 | equals([ 202 | [...'ab'.codeUnits] 203 | ])); 204 | 205 | expect(() => sink.addSlice('ab%', 0, 3, true), throwsFormatException); 206 | }); 207 | 208 | test('rejects danging digit detected in addSlice()', () { 209 | sink.addSlice('ab%2', 0, 3, false); 210 | expect( 211 | results, 212 | equals([ 213 | [...'ab'.codeUnits] 214 | ])); 215 | 216 | expect(() => sink.addSlice('ab%2', 0, 3, true), throwsFormatException); 217 | }); 218 | }); 219 | 220 | group('rejects non-ASCII character', () { 221 | for (var char in ['\u0141', '\u{10041}']) { 222 | test('"$char"', () { 223 | expect(() => percent.decode('a$char'), throwsFormatException); 224 | expect(() => percent.decode('${char}a'), throwsFormatException); 225 | 226 | var sink = percent.decoder 227 | .startChunkedConversion(StreamController(sync: true)); 228 | expect(() => sink.add(char), throwsFormatException); 229 | }); 230 | } 231 | }); 232 | 233 | test('rejects % followed by non-hex', () { 234 | expect(() => percent.decode('%z2'), throwsFormatException); 235 | expect(() => percent.decode('%2z'), throwsFormatException); 236 | }); 237 | 238 | test('rejects dangling % detected in convert()', () { 239 | expect(() => percent.decode('ab%'), throwsFormatException); 240 | }); 241 | 242 | test('rejects dangling digit detected in convert()', () { 243 | expect(() => percent.decode('ab%2'), throwsFormatException); 244 | }); 245 | }); 246 | } 247 | -------------------------------------------------------------------------------- /test/string_accumulator_sink_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:convert/convert.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | late StringAccumulatorSink sink; 10 | setUp(() { 11 | sink = StringAccumulatorSink(); 12 | }); 13 | 14 | test('provides access to the concatenated string', () { 15 | expect(sink.string, isEmpty); 16 | 17 | sink.add('foo'); 18 | expect(sink.string, equals('foo')); 19 | 20 | sink.addSlice(' bar baz', 1, 4, false); 21 | expect(sink.string, equals('foobar')); 22 | }); 23 | 24 | test('clear() clears the string', () { 25 | sink.add('foo'); 26 | expect(sink.string, equals('foo')); 27 | 28 | sink.clear(); 29 | expect(sink.string, isEmpty); 30 | 31 | sink.add('bar'); 32 | expect(sink.string, equals('bar')); 33 | }); 34 | 35 | test('indicates whether the sink is closed', () { 36 | expect(sink.isClosed, isFalse); 37 | sink.close(); 38 | expect(sink.isClosed, isTrue); 39 | }); 40 | 41 | test('indicates whether the sink is closed via addSlice', () { 42 | expect(sink.isClosed, isFalse); 43 | sink.addSlice('', 0, 0, true); 44 | expect(sink.isClosed, isTrue); 45 | }); 46 | 47 | test("doesn't allow add() to be called after close()", () { 48 | sink.close(); 49 | expect(() => sink.add('x'), throwsStateError); 50 | }); 51 | 52 | test("doesn't allow addSlice() to be called after close()", () { 53 | sink.close(); 54 | expect(() => sink.addSlice('', 0, 0, false), throwsStateError); 55 | }); 56 | } 57 | --------------------------------------------------------------------------------