├── .github ├── dependabot.yaml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib ├── http_parser.dart └── src │ ├── authentication_challenge.dart │ ├── case_insensitive_map.dart │ ├── chunked_coding.dart │ ├── chunked_coding │ ├── charcodes.dart │ ├── decoder.dart │ └── encoder.dart │ ├── http_date.dart │ ├── media_type.dart │ ├── scan.dart │ └── utils.dart ├── pubspec.yaml └── test ├── authentication_challenge_test.dart ├── case_insensitive_map_test.dart ├── chunked_coding_test.dart ├── example_test.dart ├── http_date_test.dart └── media_type_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 | permissions: read-all 15 | 16 | jobs: 17 | # Check code formatting and static analysis on a single OS (linux) 18 | # against Dart dev. 19 | analyze: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | sdk: [dev] 25 | steps: 26 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 27 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 28 | with: 29 | sdk: ${{ matrix.sdk }} 30 | - id: install 31 | name: Install dependencies 32 | run: dart pub get 33 | - name: Check formatting 34 | run: dart format --output=none --set-exit-if-changed . 35 | if: always() && steps.install.outcome == 'success' 36 | - name: Analyze code 37 | run: dart analyze --fatal-infos 38 | if: always() && steps.install.outcome == 'success' 39 | 40 | test: 41 | needs: analyze 42 | runs-on: ${{ matrix.os }} 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | os: [ubuntu-latest] 47 | sdk: [3.4, dev] 48 | steps: 49 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 50 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 51 | with: 52 | sdk: ${{ matrix.sdk }} 53 | - id: install 54 | name: Install dependencies 55 | run: dart pub get 56 | - name: Run VM tests 57 | run: dart test --platform vm 58 | if: always() && steps.install.outcome == 'success' 59 | - name: Run Chrome tests 60 | run: dart test --platform chrome 61 | if: always() && steps.install.outcome == 'success' 62 | - name: Run Chrome tests - wasm 63 | run: dart test --platform chrome --compiler dart2wasm 64 | if: always() && steps.install.outcome == 'success' 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | pubspec.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.1.0 2 | 3 | * `CaseInsensitiveMap`: added constructor `fromEntries`. 4 | * Require `package:collection` `^1.19.0` 5 | * Require Dart `^3.4.0` 6 | 7 | ## 4.0.2 8 | 9 | * Remove `package:charcode` from dev_dependencies. 10 | 11 | ## 4.0.1 12 | 13 | * Remove dependency on `package:charcode`. 14 | 15 | ## 4.0.0 16 | 17 | * Stable null safety stable release. 18 | 19 | ## 4.0.0-nullsafety 20 | 21 | * Migrate to null safety. 22 | 23 | ## 3.1.4 24 | 25 | * Fixed lints affecting package health score. 26 | * Added an example. 27 | 28 | ## 3.1.3 29 | 30 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 31 | 32 | ## 3.1.2 33 | 34 | * Require Dart SDK 2.0.0-dev.17.0 or greater. 35 | 36 | * A number of strong-mode fixes. 37 | 38 | ## 3.1.1 39 | 40 | * Fix a logic bug in the `chunkedCoding` codec. It had been producing invalid 41 | output and rejecting valid input. 42 | 43 | ## 3.1.0 44 | 45 | * Add `chunkedCoding`, a `Codec` that supports encoding and decoding the 46 | [chunked transfer coding][]. 47 | 48 | [chunked transfer coding]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 49 | 50 | ## 3.0.2 51 | 52 | * Support `string_scanner` 1.0.0. 53 | 54 | ## 3.0.1 55 | 56 | * Remove unnecessary dependencies. 57 | 58 | ## 3.0.0 59 | 60 | * All deprecated APIs have been removed. No new APIs have been added. Packages 61 | that would use 3.0.0 as a lower bound should use 2.2.0 instead—for example, 62 | `http_parser: ">=2.2.0 <4.0.0"`. 63 | 64 | * Fix all strong-mode warnings. 65 | 66 | ## 2.2.1 67 | 68 | * Add support for `crypto` 1.0.0. 69 | 70 | ## 2.2.0 71 | 72 | * `WebSocketChannel` has been moved to 73 | [the `web_socket_channel` package][web_socket_channel]. The implementation 74 | here is now deprecated. 75 | 76 | [web_socket_channel]: https://pub.dev/packages/web_socket_channel 77 | 78 | ## 2.1.0 79 | 80 | * Added `WebSocketChannel`, an implementation of `StreamChannel` that's backed 81 | by a `WebSocket`. 82 | 83 | * Deprecated `CompatibleWebSocket` in favor of `WebSocketChannel`. 84 | 85 | ## 2.0.0 86 | 87 | * Removed the `DataUri` class. It's redundant with the `Uri.data` getter that's 88 | coming in Dart 1.14, and the `DataUri.data` field in particular was an invalid 89 | override of that field. 90 | 91 | ## 1.1.0 92 | 93 | * The MIME spec says that media types and their parameter names are 94 | case-insensitive. Accordingly, `MediaType` now uses a case-insensitive map for 95 | its parameters and its `type` and `subtype` fields are now always lowercase. 96 | 97 | ## 1.0.0 98 | 99 | This is 1.0.0 because the API is stable—there are no breaking changes. 100 | 101 | * Added an `AuthenticationChallenge` class for parsing and representing the 102 | value of `WWW-Authenticate` and related headers. 103 | 104 | * Added a `CaseInsensitiveMap` class for representing case-insensitive HTTP 105 | values. 106 | 107 | ## 0.0.2+8 108 | 109 | * Bring in the latest `dart:io` WebSocket code. 110 | 111 | ## 0.0.2+7 112 | 113 | * Add more detail to the readme. 114 | 115 | ## 0.0.2+6 116 | 117 | * Updated homepage URL. 118 | 119 | ## 0.0.2+5 120 | 121 | * Widen the version constraint on the `collection` package. 122 | 123 | ## 0.0.2+4 124 | 125 | * Widen the `string_scanner` version constraint. 126 | 127 | ## 0.0.2+3 128 | 129 | * Fix a library name conflict. 130 | 131 | ## 0.0.2+2 132 | 133 | * Fixes for HTTP date formatting. 134 | 135 | ## 0.0.2+1 136 | 137 | * Minor code refactoring. 138 | 139 | ## 0.0.2 140 | 141 | * Added `CompatibleWebSocket`, for platform- and API-independent support for the 142 | WebSocket API. 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, 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/http/tree/master/pkgs/http_parser 3 | 4 | [![Build Status](https://github.com/dart-lang/http_parser/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/http_parser/actions?query=workflow%3A"Dart+CI"+branch%3Amaster) 5 | [![Pub Package](https://img.shields.io/pub/v/http_parser.svg)](https://pub.dartlang.org/packages/http_parser) 6 | [![package publisher](https://img.shields.io/pub/publisher/http_parser.svg)](https://pub.dev/packages/http_parser/publisher) 7 | 8 | `http_parser` is a platform-independent package for parsing and serializing 9 | various HTTP-related formats. It's designed to be usable on both the browser and 10 | the server, and thus avoids referencing any types from `dart:io` or `dart:html`. 11 | 12 | ## Features 13 | 14 | * Support for parsing and formatting dates according to [HTTP/1.1][2616], the 15 | HTTP/1.1 standard. 16 | 17 | * A `MediaType` class that represents an HTTP media type, as used in `Accept` 18 | and `Content-Type` headers. This class supports both parsing and formatting 19 | media types according to [HTTP/1.1][2616]. 20 | 21 | * A `WebSocketChannel` class that provides a `StreamChannel` interface for both 22 | the client and server sides of the [WebSocket protocol][6455] independently of 23 | any specific server implementation. 24 | 25 | [2616]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html 26 | [6455]: https://tools.ietf.org/html/rfc6455 27 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/tools/analysis#the-analysis-options-file 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - avoid_bool_literals_in_conditional_expressions 13 | - avoid_classes_with_only_static_members 14 | - avoid_private_typedef_functions 15 | - avoid_redundant_argument_values 16 | - avoid_returning_this 17 | - avoid_unused_constructor_parameters 18 | - avoid_void_async 19 | - cancel_subscriptions 20 | - join_return_with_assignment 21 | - literal_only_boolean_expressions 22 | - missing_whitespace_between_adjacent_strings 23 | - no_adjacent_strings_in_list 24 | - no_runtimeType_toString 25 | - package_api_docs 26 | - prefer_const_declarations 27 | - prefer_expression_function_bodies 28 | - prefer_final_locals 29 | - unnecessary_await_in_return 30 | - unnecessary_breaks 31 | - use_if_null_to_convert_nulls_to_bools 32 | - use_raw_strings 33 | - use_string_buffers 34 | -------------------------------------------------------------------------------- /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 'package:http_parser/http_parser.dart'; 6 | 7 | void main() { 8 | final date = DateTime.utc(2014, 9, 9, 9, 9, 9); 9 | print(date); // 2014-09-09 09:09:09.000Z 10 | 11 | final httpDateFormatted = formatHttpDate(date); 12 | print(httpDateFormatted); // Tue, 09 Sep 2014 09:09:09 GMT 13 | 14 | final nowParsed = parseHttpDate(httpDateFormatted); 15 | print(nowParsed); // 2014-09-09 09:09:09.000Z 16 | } 17 | -------------------------------------------------------------------------------- /lib/http_parser.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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/authentication_challenge.dart'; 6 | export 'src/case_insensitive_map.dart'; 7 | export 'src/chunked_coding.dart'; 8 | export 'src/http_date.dart'; 9 | export 'src/media_type.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/authentication_challenge.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:collection'; 6 | 7 | import 'package:string_scanner/string_scanner.dart'; 8 | 9 | import 'case_insensitive_map.dart'; 10 | import 'scan.dart'; 11 | import 'utils.dart'; 12 | 13 | /// A single challenge in a WWW-Authenticate header, parsed as per [RFC 2617][]. 14 | /// 15 | /// [RFC 2617]: http://tools.ietf.org/html/rfc2617 16 | /// 17 | /// Each WWW-Authenticate header contains one or more challenges, representing 18 | /// valid ways to authenticate with the server. 19 | class AuthenticationChallenge { 20 | /// The scheme describing the type of authentication that's required, for 21 | /// example "basic" or "digest". 22 | /// 23 | /// This is normalized to always be lower-case. 24 | final String scheme; 25 | 26 | /// The parameters describing how to authenticate. 27 | /// 28 | /// The semantics of these parameters are scheme-specific. The keys of this 29 | /// map are case-insensitive. 30 | final Map parameters; 31 | 32 | /// Parses a WWW-Authenticate header, which should contain one or more 33 | /// challenges. 34 | /// 35 | /// Throws a [FormatException] if the header is invalid. 36 | static List parseHeader(String header) => 37 | wrapFormatException('authentication header', header, () { 38 | final scanner = StringScanner(header); 39 | scanner.scan(whitespace); 40 | final challenges = parseList(scanner, () { 41 | final scheme = _scanScheme(scanner, whitespaceName: '" " or "="'); 42 | 43 | // Manually parse the inner list. We need to do some lookahead to 44 | // disambiguate between an auth param and another challenge. 45 | final params = {}; 46 | 47 | // Consume initial empty values. 48 | while (scanner.scan(',')) { 49 | scanner.scan(whitespace); 50 | } 51 | 52 | _scanAuthParam(scanner, params); 53 | 54 | var beforeComma = scanner.position; 55 | while (scanner.scan(',')) { 56 | scanner.scan(whitespace); 57 | 58 | // Empty elements are allowed, but excluded from the results. 59 | if (scanner.matches(',') || scanner.isDone) continue; 60 | 61 | scanner.expect(token, name: 'a token'); 62 | final name = scanner.lastMatch![0]!; 63 | scanner.scan(whitespace); 64 | 65 | // If there's no "=", then this is another challenge rather than a 66 | // parameter for the current challenge. 67 | if (!scanner.scan('=')) { 68 | scanner.position = beforeComma; 69 | break; 70 | } 71 | 72 | scanner.scan(whitespace); 73 | 74 | if (scanner.scan(token)) { 75 | params[name] = scanner.lastMatch![0]!; 76 | } else { 77 | params[name] = expectQuotedString(scanner, 78 | name: 'a token or a quoted string'); 79 | } 80 | 81 | scanner.scan(whitespace); 82 | beforeComma = scanner.position; 83 | } 84 | 85 | return AuthenticationChallenge(scheme, params); 86 | }); 87 | 88 | scanner.expectDone(); 89 | return challenges; 90 | }); 91 | 92 | /// Parses a single WWW-Authenticate challenge value. 93 | /// 94 | /// Throws a [FormatException] if the challenge is invalid. 95 | factory AuthenticationChallenge.parse(String challenge) => 96 | wrapFormatException('authentication challenge', challenge, () { 97 | final scanner = StringScanner(challenge); 98 | scanner.scan(whitespace); 99 | final scheme = _scanScheme(scanner); 100 | 101 | final params = {}; 102 | parseList(scanner, () => _scanAuthParam(scanner, params)); 103 | 104 | scanner.expectDone(); 105 | return AuthenticationChallenge(scheme, params); 106 | }); 107 | 108 | /// Scans a single scheme name and asserts that it's followed by a space. 109 | /// 110 | /// If [whitespaceName] is passed, it's used as the name for exceptions thrown 111 | /// due to invalid trailing whitespace. 112 | static String _scanScheme(StringScanner scanner, {String? whitespaceName}) { 113 | scanner.expect(token, name: 'a token'); 114 | final scheme = scanner.lastMatch![0]!.toLowerCase(); 115 | 116 | scanner.scan(whitespace); 117 | 118 | // The spec specifically requires a space between the scheme and its 119 | // params. 120 | if (scanner.lastMatch == null || !scanner.lastMatch![0]!.contains(' ')) { 121 | scanner.expect(' ', name: whitespaceName); 122 | } 123 | 124 | return scheme; 125 | } 126 | 127 | /// Scans a single authentication parameter and stores its result in [params]. 128 | static void _scanAuthParam( 129 | StringScanner scanner, Map params) { 130 | scanner.expect(token, name: 'a token'); 131 | final name = scanner.lastMatch![0]!; 132 | scanner.scan(whitespace); 133 | scanner.expect('='); 134 | scanner.scan(whitespace); 135 | 136 | if (scanner.scan(token)) { 137 | params[name] = scanner.lastMatch![0]!; 138 | } else { 139 | params[name] = 140 | expectQuotedString(scanner, name: 'a token or a quoted string'); 141 | } 142 | 143 | scanner.scan(whitespace); 144 | } 145 | 146 | /// Creates a new challenge value with [scheme] and [parameters]. 147 | AuthenticationChallenge(this.scheme, Map parameters) 148 | : parameters = UnmodifiableMapView(CaseInsensitiveMap.from(parameters)); 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/case_insensitive_map.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 'package:collection/collection.dart'; 6 | 7 | /// A map from case-insensitive strings to values. 8 | /// 9 | /// Much of HTTP is case-insensitive, so this is useful to have pre-defined. 10 | class CaseInsensitiveMap extends CanonicalizedMap { 11 | /// Creates an empty case-insensitive map. 12 | CaseInsensitiveMap() : super(_canonicalizer); 13 | 14 | /// Creates a case-insensitive map that is initialized with the key/value 15 | /// pairs of [other]. 16 | CaseInsensitiveMap.from(Map other) 17 | : super.from(other, _canonicalizer); 18 | 19 | /// Creates a case-insensitive map that is initialized with the key/value 20 | /// pairs of [entries]. 21 | CaseInsensitiveMap.fromEntries(Iterable> entries) 22 | : super.fromEntries(entries, _canonicalizer); 23 | 24 | static String _canonicalizer(String key) => key.toLowerCase(); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/chunked_coding.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 | import 'chunked_coding/decoder.dart'; 8 | import 'chunked_coding/encoder.dart'; 9 | 10 | export 'chunked_coding/decoder.dart' hide chunkedCodingDecoder; 11 | export 'chunked_coding/encoder.dart' hide chunkedCodingEncoder; 12 | 13 | /// The canonical instance of [ChunkedCodingCodec]. 14 | const chunkedCoding = ChunkedCodingCodec._(); 15 | 16 | /// A codec that encodes and decodes the [chunked transfer coding][]. 17 | /// 18 | /// [chunked transfer coding]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1 19 | /// 20 | /// The [encoder] creates a *single* chunked message for each call to 21 | /// [ChunkedCodingEncoder.convert] or 22 | /// [ChunkedCodingEncoder.startChunkedConversion]. This means that it will 23 | /// always add an end-of-message footer once conversion has finished. It doesn't 24 | /// support generating chunk extensions or trailing headers. 25 | /// 26 | /// Similarly, the [decoder] decodes a *single* chunked message into a stream of 27 | /// byte arrays that must be concatenated to get the full list (like most Dart 28 | /// byte streams). It doesn't support decoding a stream that contains multiple 29 | /// chunked messages, nor does it support a stream that contains chunked data 30 | /// mixed with other types of data. 31 | /// 32 | /// Currently, [decoder] will fail to parse chunk extensions and trailing 33 | /// headers. It may be updated to silently ignore them in the future. 34 | class ChunkedCodingCodec extends Codec, List> { 35 | @override 36 | ChunkedCodingEncoder get encoder => chunkedCodingEncoder; 37 | 38 | @override 39 | ChunkedCodingDecoder get decoder => chunkedCodingDecoder; 40 | 41 | const ChunkedCodingCodec._(); 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/chunked_coding/charcodes.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, 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 | /// "Line feed" control character. 6 | const int $lf = 0x0a; 7 | 8 | /// "Carriage return" control character. 9 | const int $cr = 0x0d; 10 | 11 | /// Character `0`. 12 | const int $0 = 0x30; 13 | 14 | /// Character `1`. 15 | const int $1 = 0x31; 16 | 17 | /// Character `3`. 18 | const int $3 = 0x33; 19 | 20 | /// Character `4`. 21 | const int $4 = 0x34; 22 | 23 | /// Character `7`. 24 | const int $7 = 0x37; 25 | 26 | /// Character `A`. 27 | const int $A = 0x41; 28 | 29 | /// Character `q`. 30 | const int $q = 0x71; 31 | 32 | /// Character `a`. 33 | const int $a = 0x61; 34 | 35 | /// Character `f`. 36 | const int $f = 0x66; 37 | -------------------------------------------------------------------------------- /lib/src/chunked_coding/decoder.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:async'; 6 | import 'dart:convert'; 7 | import 'dart:math' as math; 8 | import 'dart:typed_data'; 9 | 10 | import 'package:typed_data/typed_data.dart'; 11 | 12 | import 'charcodes.dart'; 13 | 14 | /// The canonical instance of [ChunkedCodingDecoder]. 15 | const chunkedCodingDecoder = ChunkedCodingDecoder._(); 16 | 17 | /// A converter that decodes byte arrays into chunks with size tags. 18 | class ChunkedCodingDecoder extends Converter, List> { 19 | const ChunkedCodingDecoder._(); 20 | 21 | @override 22 | List convert(List input) { 23 | final sink = _Sink(StreamController()); 24 | final output = sink._decode(input, 0, input.length); 25 | if (sink._state == _State.end) return output; 26 | 27 | throw FormatException('Input ended unexpectedly.', input, input.length); 28 | } 29 | 30 | @override 31 | ByteConversionSink startChunkedConversion(Sink> sink) => 32 | _Sink(sink); 33 | } 34 | 35 | /// A conversion sink for the chunked transfer encoding. 36 | class _Sink extends ByteConversionSinkBase { 37 | /// The underlying sink to which decoded byte arrays will be passed. 38 | final Sink> _sink; 39 | 40 | /// The current state of the sink's parsing. 41 | var _state = _State.boundary; 42 | 43 | /// The size of the chunk being parsed. 44 | /// 45 | /// Only assigned and used within [_decode]. 46 | late int _size; 47 | 48 | _Sink(this._sink); 49 | 50 | @override 51 | void add(List chunk) => addSlice(chunk, 0, chunk.length, false); 52 | 53 | @override 54 | void addSlice(List chunk, int start, int end, bool isLast) { 55 | RangeError.checkValidRange(start, end, chunk.length); 56 | final output = _decode(chunk, start, end); 57 | if (output.isNotEmpty) _sink.add(output); 58 | if (isLast) _close(chunk, end); 59 | } 60 | 61 | @override 62 | void close() => _close(); 63 | 64 | /// Like [close], but includes [chunk] and [index] in the [FormatException] if 65 | /// one is thrown. 66 | void _close([List? chunk, int? index]) { 67 | if (_state != _State.end) { 68 | throw FormatException('Input ended unexpectedly.', chunk, index); 69 | } 70 | 71 | _sink.close(); 72 | } 73 | 74 | /// Decodes the data in [bytes] from [start] to [end]. 75 | Uint8List _decode(List bytes, int start, int end) { 76 | /// Throws a [FormatException] if `bytes[start] != $char`. Uses [name] to 77 | /// describe the character in the exception text. 78 | void assertCurrentChar(int char, String name) { 79 | if (bytes[start] != char) { 80 | throw FormatException('Expected $name.', bytes, start); 81 | } 82 | } 83 | 84 | final buffer = Uint8Buffer(); 85 | while (start != end) { 86 | switch (_state) { 87 | case _State.boundary: 88 | _size = _digitForByte(bytes, start); 89 | _state = _State.size; 90 | start++; 91 | 92 | case _State.size: 93 | if (bytes[start] == $cr) { 94 | _state = _State.sizeBeforeLF; 95 | } else { 96 | // Shift four bits left since a single hex digit contains four bits 97 | // of information. 98 | _size = (_size << 4) + _digitForByte(bytes, start); 99 | } 100 | start++; 101 | 102 | case _State.sizeBeforeLF: 103 | assertCurrentChar($lf, 'LF'); 104 | _state = _size == 0 ? _State.endBeforeCR : _State.body; 105 | start++; 106 | 107 | case _State.body: 108 | final chunkEnd = math.min(end, start + _size); 109 | buffer.addAll(bytes, start, chunkEnd); 110 | _size -= chunkEnd - start; 111 | start = chunkEnd; 112 | if (_size == 0) _state = _State.bodyBeforeCR; 113 | 114 | case _State.bodyBeforeCR: 115 | assertCurrentChar($cr, 'CR'); 116 | _state = _State.bodyBeforeLF; 117 | start++; 118 | 119 | case _State.bodyBeforeLF: 120 | assertCurrentChar($lf, 'LF'); 121 | _state = _State.boundary; 122 | start++; 123 | 124 | case _State.endBeforeCR: 125 | assertCurrentChar($cr, 'CR'); 126 | _state = _State.endBeforeLF; 127 | start++; 128 | 129 | case _State.endBeforeLF: 130 | assertCurrentChar($lf, 'LF'); 131 | _state = _State.end; 132 | start++; 133 | 134 | case _State.end: 135 | throw FormatException('Expected no more data.', bytes, start); 136 | } 137 | } 138 | return buffer.buffer.asUint8List(0, buffer.length); 139 | } 140 | 141 | /// Returns the hex digit (0 through 15) corresponding to the byte at index 142 | /// [index] in [bytes]. 143 | /// 144 | /// If the given byte isn't a hexadecimal ASCII character, throws a 145 | /// [FormatException]. 146 | int _digitForByte(List bytes, int index) { 147 | // If the byte is a numeral, get its value. XOR works because 0 in ASCII is 148 | // `0b110000` and the other numerals come after it in ascending order and 149 | // take up at most four bits. 150 | // 151 | // We check for digits first because it ensures there's only a single branch 152 | // for 10 out of 16 of the expected cases. We don't count the `digit >= 0` 153 | // check because branch prediction will always work on it for valid data. 154 | final byte = bytes[index]; 155 | final digit = $0 ^ byte; 156 | if (digit <= 9) { 157 | if (digit >= 0) return digit; 158 | } else { 159 | // If the byte is an uppercase letter, convert it to lowercase. This works 160 | // because uppercase letters in ASCII are exactly `0b100000 = 0x20` less 161 | // than lowercase letters, so if we ensure that that bit is 1 we ensure 162 | // that the letter is lowercase. 163 | final letter = 0x20 | byte; 164 | if ($a <= letter && letter <= $f) return letter - $a + 10; 165 | } 166 | 167 | throw FormatException( 168 | 'Invalid hexadecimal byte 0x${byte.toRadixString(16).toUpperCase()}.', 169 | bytes, 170 | index); 171 | } 172 | } 173 | 174 | /// An enumeration of states that [_Sink] can exist in when decoded a chunked 175 | /// message. 176 | enum _State { 177 | /// The parser has fully parsed one chunk and is expecting the header for the 178 | /// next chunk. 179 | /// 180 | /// Transitions to [size]. 181 | boundary('boundary'), 182 | 183 | /// The parser has parsed at least one digit of the chunk size header, but has 184 | /// not yet parsed the `CR LF` sequence that indicates the end of that header. 185 | /// 186 | /// Transitions to [sizeBeforeLF]. 187 | size('size'), 188 | 189 | /// The parser has parsed the chunk size header and the CR character after it, 190 | /// but not the LF. 191 | /// 192 | /// Transitions to [body] or [bodyBeforeCR]. 193 | sizeBeforeLF('size before LF'), 194 | 195 | /// The parser has parsed a chunk header and possibly some of the body, but 196 | /// still needs to consume more bytes. 197 | /// 198 | /// Transitions to [bodyBeforeCR]. 199 | body('body'), 200 | 201 | // The parser has parsed all the bytes in a chunk body but not the CR LF 202 | // sequence that follows it. 203 | // 204 | // Transitions to [bodyBeforeLF]. 205 | bodyBeforeCR('body before CR'), 206 | 207 | // The parser has parsed all the bytes in a chunk body and the CR that follows 208 | // it, but not the LF after that. 209 | // 210 | // Transitions to [boundary]. 211 | bodyBeforeLF('body before LF'), 212 | 213 | /// The parser has parsed the final empty chunk but not the CR LF sequence 214 | /// that follows it. 215 | /// 216 | /// Transitions to [endBeforeLF]. 217 | endBeforeCR('end before CR'), 218 | 219 | /// The parser has parsed the final empty chunk and the CR that follows it, 220 | /// but not the LF after that. 221 | /// 222 | /// Transitions to [end]. 223 | endBeforeLF('end before LF'), 224 | 225 | /// The parser has parsed the final empty chunk as well as the CR LF that 226 | /// follows, and expects no more data. 227 | end('end'); 228 | 229 | const _State(this.name); 230 | 231 | final String name; 232 | 233 | @override 234 | String toString() => name; 235 | } 236 | -------------------------------------------------------------------------------- /lib/src/chunked_coding/encoder.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 'charcodes.dart'; 9 | 10 | /// The canonical instance of [ChunkedCodingEncoder]. 11 | const chunkedCodingEncoder = ChunkedCodingEncoder._(); 12 | 13 | /// The chunk indicating that the chunked message has finished. 14 | final _doneChunk = Uint8List.fromList([$0, $cr, $lf, $cr, $lf]); 15 | 16 | /// A converter that encodes byte arrays into chunks with size tags. 17 | class ChunkedCodingEncoder extends Converter, List> { 18 | const ChunkedCodingEncoder._(); 19 | 20 | @override 21 | List convert(List input) => 22 | _convert(input, 0, input.length, isLast: true); 23 | 24 | @override 25 | ByteConversionSink startChunkedConversion(Sink> sink) => 26 | _Sink(sink); 27 | } 28 | 29 | /// A conversion sink for the chunked transfer encoding. 30 | class _Sink extends ByteConversionSinkBase { 31 | /// The underlying sink to which encoded byte arrays will be passed. 32 | final Sink> _sink; 33 | 34 | _Sink(this._sink); 35 | 36 | @override 37 | void add(List chunk) { 38 | _sink.add(_convert(chunk, 0, chunk.length)); 39 | } 40 | 41 | @override 42 | void addSlice(List chunk, int start, int end, bool isLast) { 43 | RangeError.checkValidRange(start, end, chunk.length); 44 | _sink.add(_convert(chunk, start, end, isLast: isLast)); 45 | if (isLast) _sink.close(); 46 | } 47 | 48 | @override 49 | void close() { 50 | _sink.add(_doneChunk); 51 | _sink.close(); 52 | } 53 | } 54 | 55 | /// Returns a new list a chunked transfer encoding header followed by the slice 56 | /// of [bytes] from [start] to [end]. 57 | /// 58 | /// If [isLast] is `true`, this adds the footer that indicates that the chunked 59 | /// message is complete. 60 | List _convert(List bytes, int start, int end, {bool isLast = false}) { 61 | if (end == start) return isLast ? _doneChunk : const []; 62 | 63 | final size = end - start; 64 | final sizeInHex = size.toRadixString(16); 65 | final footerSize = isLast ? _doneChunk.length : 0; 66 | 67 | // Add 4 for the CRLF sequences that follow the size header and the bytes. 68 | final list = Uint8List(sizeInHex.length + 4 + size + footerSize); 69 | list.setRange(0, sizeInHex.length, sizeInHex.codeUnits); 70 | 71 | var cursor = sizeInHex.length; 72 | list[cursor++] = $cr; 73 | list[cursor++] = $lf; 74 | list.setRange(cursor, cursor + end - start, bytes, start); 75 | cursor += end - start; 76 | list[cursor++] = $cr; 77 | list[cursor++] = $lf; 78 | 79 | if (isLast) { 80 | list.setRange(list.length - footerSize, list.length, _doneChunk); 81 | } 82 | return list; 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/http_date.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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:string_scanner/string_scanner.dart'; 6 | 7 | import 'utils.dart'; 8 | 9 | const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; 10 | const _months = [ 11 | 'Jan', 12 | 'Feb', 13 | 'Mar', 14 | 'Apr', 15 | 'May', 16 | 'Jun', 17 | 'Jul', 18 | 'Aug', 19 | 'Sep', 20 | 'Oct', 21 | 'Nov', 22 | 'Dec' 23 | ]; 24 | 25 | final _shortWeekdayRegExp = RegExp(r'Mon|Tue|Wed|Thu|Fri|Sat|Sun'); 26 | final _longWeekdayRegExp = 27 | RegExp(r'Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday'); 28 | final _monthRegExp = RegExp(r'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec'); 29 | final _digitRegExp = RegExp(r'\d+'); 30 | 31 | /// Return a HTTP-formatted string representation of [date]. 32 | /// 33 | /// This follows [RFC 822](http://tools.ietf.org/html/rfc822) as updated by 34 | /// [RFC 1123](http://tools.ietf.org/html/rfc1123). 35 | String formatHttpDate(DateTime date) { 36 | date = date.toUtc(); 37 | final buffer = StringBuffer() 38 | ..write(_weekdays[date.weekday - 1]) 39 | ..write(', ') 40 | ..write(date.day <= 9 ? '0' : '') 41 | ..write(date.day.toString()) 42 | ..write(' ') 43 | ..write(_months[date.month - 1]) 44 | ..write(' ') 45 | ..write(date.year.toString()) 46 | ..write(date.hour <= 9 ? ' 0' : ' ') 47 | ..write(date.hour.toString()) 48 | ..write(date.minute <= 9 ? ':0' : ':') 49 | ..write(date.minute.toString()) 50 | ..write(date.second <= 9 ? ':0' : ':') 51 | ..write(date.second.toString()) 52 | ..write(' GMT'); 53 | return buffer.toString(); 54 | } 55 | 56 | /// Parses an HTTP-formatted date into a UTC [DateTime]. 57 | /// 58 | /// This follows [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3). 59 | /// It will throw a [FormatException] if [date] is invalid. 60 | DateTime parseHttpDate(String date) => 61 | wrapFormatException('HTTP date', date, () { 62 | final scanner = StringScanner(date); 63 | 64 | if (scanner.scan(_longWeekdayRegExp)) { 65 | // RFC 850 starts with a long weekday. 66 | scanner.expect(', '); 67 | final day = _parseInt(scanner, 2); 68 | scanner.expect('-'); 69 | final month = _parseMonth(scanner); 70 | scanner.expect('-'); 71 | final year = 1900 + _parseInt(scanner, 2); 72 | scanner.expect(' '); 73 | final time = _parseTime(scanner); 74 | scanner.expect(' GMT'); 75 | scanner.expectDone(); 76 | 77 | return _makeDateTime(year, month, day, time); 78 | } 79 | 80 | // RFC 1123 and asctime both start with a short weekday. 81 | scanner.expect(_shortWeekdayRegExp); 82 | if (scanner.scan(', ')) { 83 | // RFC 1123 follows the weekday with a comma. 84 | final day = _parseInt(scanner, 2); 85 | scanner.expect(' '); 86 | final month = _parseMonth(scanner); 87 | scanner.expect(' '); 88 | final year = _parseInt(scanner, 4); 89 | scanner.expect(' '); 90 | final time = _parseTime(scanner); 91 | scanner.expect(' GMT'); 92 | scanner.expectDone(); 93 | 94 | return _makeDateTime(year, month, day, time); 95 | } 96 | 97 | // asctime follows the weekday with a space. 98 | scanner.expect(' '); 99 | final month = _parseMonth(scanner); 100 | scanner.expect(' '); 101 | final day = 102 | scanner.scan(' ') ? _parseInt(scanner, 1) : _parseInt(scanner, 2); 103 | scanner.expect(' '); 104 | final time = _parseTime(scanner); 105 | scanner.expect(' '); 106 | final year = _parseInt(scanner, 4); 107 | scanner.expectDone(); 108 | 109 | return _makeDateTime(year, month, day, time); 110 | }); 111 | 112 | /// Parses a short-form month name to a form accepted by [DateTime]. 113 | int _parseMonth(StringScanner scanner) { 114 | scanner.expect(_monthRegExp); 115 | // DateTime uses 1-indexed months. 116 | return _months.indexOf(scanner.lastMatch![0]!) + 1; 117 | } 118 | 119 | /// Parses an int an enforces that it has exactly [digits] digits. 120 | int _parseInt(StringScanner scanner, int digits) { 121 | scanner.expect(_digitRegExp); 122 | if (scanner.lastMatch![0]!.length != digits) { 123 | scanner.error('expected a $digits-digit number.'); 124 | } 125 | 126 | return int.parse(scanner.lastMatch![0]!); 127 | } 128 | 129 | /// Parses an timestamp of the form "HH:MM:SS" on a 24-hour clock. 130 | DateTime _parseTime(StringScanner scanner) { 131 | final hours = _parseInt(scanner, 2); 132 | if (hours >= 24) scanner.error('hours may not be greater than 24.'); 133 | scanner.expect(':'); 134 | 135 | final minutes = _parseInt(scanner, 2); 136 | if (minutes >= 60) scanner.error('minutes may not be greater than 60.'); 137 | scanner.expect(':'); 138 | 139 | final seconds = _parseInt(scanner, 2); 140 | if (seconds >= 60) scanner.error('seconds may not be greater than 60.'); 141 | 142 | return DateTime(1, 1, 1, hours, minutes, seconds); 143 | } 144 | 145 | /// Returns a UTC [DateTime] from the given components. 146 | /// 147 | /// Validates that [day] is a valid day for [month]. If it's not, throws a 148 | /// [FormatException]. 149 | DateTime _makeDateTime(int year, int month, int day, DateTime time) { 150 | final dateTime = 151 | DateTime.utc(year, month, day, time.hour, time.minute, time.second); 152 | 153 | // If [day] was too large, it will cause [month] to overflow. 154 | if (dateTime.month != month) { 155 | throw FormatException("invalid day '$day' for month '$month'."); 156 | } 157 | return dateTime; 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/media_type.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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:collection/collection.dart'; 6 | import 'package:string_scanner/string_scanner.dart'; 7 | 8 | import 'case_insensitive_map.dart'; 9 | import 'scan.dart'; 10 | import 'utils.dart'; 11 | 12 | /// A regular expression matching a character that needs to be backslash-escaped 13 | /// in a quoted string. 14 | final _escapedChar = RegExp(r'["\x00-\x1F\x7F]'); 15 | 16 | /// A class representing an HTTP media type, as used in Accept and Content-Type 17 | /// headers. 18 | /// 19 | /// This is immutable; new instances can be created based on an old instance by 20 | /// calling [change]. 21 | class MediaType { 22 | /// The primary identifier of the MIME type. 23 | /// 24 | /// This is always lowercase. 25 | final String type; 26 | 27 | /// The secondary identifier of the MIME type. 28 | /// 29 | /// This is always lowercase. 30 | final String subtype; 31 | 32 | /// The parameters to the media type. 33 | /// 34 | /// This map is immutable and the keys are case-insensitive. 35 | final Map parameters; 36 | 37 | /// The media type's MIME type. 38 | String get mimeType => '$type/$subtype'; 39 | 40 | /// Parses a media type. 41 | /// 42 | /// This will throw a FormatError if the media type is invalid. 43 | factory MediaType.parse(String mediaType) => 44 | // This parsing is based on sections 3.6 and 3.7 of the HTTP spec: 45 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html. 46 | wrapFormatException('media type', mediaType, () { 47 | final scanner = StringScanner(mediaType); 48 | scanner.scan(whitespace); 49 | scanner.expect(token); 50 | final type = scanner.lastMatch![0]!; 51 | scanner.expect('/'); 52 | scanner.expect(token); 53 | final subtype = scanner.lastMatch![0]!; 54 | scanner.scan(whitespace); 55 | 56 | final parameters = {}; 57 | while (scanner.scan(';')) { 58 | scanner.scan(whitespace); 59 | scanner.expect(token); 60 | final attribute = scanner.lastMatch![0]!; 61 | scanner.expect('='); 62 | 63 | String value; 64 | if (scanner.scan(token)) { 65 | value = scanner.lastMatch![0]!; 66 | } else { 67 | value = expectQuotedString(scanner); 68 | } 69 | 70 | scanner.scan(whitespace); 71 | parameters[attribute] = value; 72 | } 73 | 74 | scanner.expectDone(); 75 | return MediaType(type, subtype, parameters); 76 | }); 77 | 78 | MediaType(String type, String subtype, [Map? parameters]) 79 | : type = type.toLowerCase(), 80 | subtype = subtype.toLowerCase(), 81 | parameters = UnmodifiableMapView( 82 | parameters == null ? {} : CaseInsensitiveMap.from(parameters)); 83 | 84 | /// Returns a copy of this [MediaType] with some fields altered. 85 | /// 86 | /// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed 87 | /// and alters both the [type] and [subtype] fields; it cannot be passed along 88 | /// with [type] or [subtype]. 89 | /// 90 | /// [parameters] overwrites and adds to the corresponding field. If 91 | /// [clearParameters] is passed, it replaces the corresponding field entirely 92 | /// instead. 93 | MediaType change( 94 | {String? type, 95 | String? subtype, 96 | String? mimeType, 97 | Map? parameters, 98 | bool clearParameters = false}) { 99 | if (mimeType != null) { 100 | if (type != null) { 101 | throw ArgumentError('You may not pass both [type] and [mimeType].'); 102 | } else if (subtype != null) { 103 | throw ArgumentError('You may not pass both [subtype] and ' 104 | '[mimeType].'); 105 | } 106 | 107 | final segments = mimeType.split('/'); 108 | if (segments.length != 2) { 109 | throw FormatException('Invalid mime type "$mimeType".'); 110 | } 111 | 112 | type = segments[0]; 113 | subtype = segments[1]; 114 | } 115 | 116 | type ??= this.type; 117 | subtype ??= this.subtype; 118 | parameters ??= {}; 119 | 120 | if (!clearParameters) { 121 | final newParameters = parameters; 122 | parameters = Map.from(this.parameters); 123 | parameters.addAll(newParameters); 124 | } 125 | 126 | return MediaType(type, subtype, parameters); 127 | } 128 | 129 | /// Converts the media type to a string. 130 | /// 131 | /// This will produce a valid HTTP media type. 132 | @override 133 | String toString() { 134 | final buffer = StringBuffer() 135 | ..write(type) 136 | ..write('/') 137 | ..write(subtype); 138 | 139 | parameters.forEach((attribute, value) { 140 | buffer.write('; $attribute='); 141 | if (nonToken.hasMatch(value)) { 142 | buffer 143 | ..write('"') 144 | ..write( 145 | value.replaceAllMapped(_escapedChar, (match) => '\\${match[0]}')) 146 | ..write('"'); 147 | } else { 148 | buffer.write(value); 149 | } 150 | }); 151 | 152 | return buffer.toString(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/src/scan.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 'package:string_scanner/string_scanner.dart'; 6 | 7 | /// An HTTP token. 8 | final token = RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+'); 9 | 10 | /// Linear whitespace. 11 | final _lws = RegExp(r'(?:\r\n)?[ \t]+'); 12 | 13 | /// A quoted string. 14 | final _quotedString = RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"'); 15 | 16 | /// A quoted pair. 17 | final _quotedPair = RegExp(r'\\(.)'); 18 | 19 | /// A character that is *not* a valid HTTP token. 20 | final nonToken = RegExp(r'[()<>@,;:"\\/\[\]?={} \t\x00-\x1F\x7F]'); 21 | 22 | /// A regular expression matching any number of [_lws] productions in a row. 23 | final whitespace = RegExp('(?:${_lws.pattern})*'); 24 | 25 | /// Parses a list of elements, as in `1#element` in the HTTP spec. 26 | /// 27 | /// [scanner] is used to parse the elements, and [parseElement] is used to parse 28 | /// each one individually. The values returned by [parseElement] are collected 29 | /// in a list and returned. 30 | /// 31 | /// Once this is finished, [scanner] will be at the next non-LWS character in 32 | /// the string, or the end of the string. 33 | List parseList(StringScanner scanner, T Function() parseElement) { 34 | final result = []; 35 | 36 | // Consume initial empty values. 37 | while (scanner.scan(',')) { 38 | scanner.scan(whitespace); 39 | } 40 | 41 | result.add(parseElement()); 42 | scanner.scan(whitespace); 43 | 44 | while (scanner.scan(',')) { 45 | scanner.scan(whitespace); 46 | 47 | // Empty elements are allowed, but excluded from the results. 48 | if (scanner.matches(',') || scanner.isDone) continue; 49 | 50 | result.add(parseElement()); 51 | scanner.scan(whitespace); 52 | } 53 | 54 | return result; 55 | } 56 | 57 | /// Parses a single quoted string, and returns its contents. 58 | /// 59 | /// If [name] is passed, it's used to describe the expected value if it's not 60 | /// found. 61 | String expectQuotedString( 62 | StringScanner scanner, { 63 | String name = 'quoted string', 64 | }) { 65 | scanner.expect(_quotedString, name: name); 66 | final string = scanner.lastMatch![0]!; 67 | return string 68 | .substring(1, string.length - 1) 69 | .replaceAllMapped(_quotedPair, (match) => match[1]!); 70 | } 71 | -------------------------------------------------------------------------------- /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 'package:source_span/source_span.dart'; 6 | 7 | /// Runs [body] and wraps any format exceptions it produces. 8 | /// 9 | /// [name] should describe the type of thing being parsed, and [value] should be 10 | /// its actual value. 11 | T wrapFormatException(String name, String value, T Function() body) { 12 | try { 13 | return body(); 14 | } on SourceSpanFormatException catch (error) { 15 | throw SourceSpanFormatException( 16 | 'Invalid $name: ${error.message}', error.span, error.source); 17 | } on FormatException catch (error) { 18 | throw FormatException( 19 | 'Invalid $name "$value": ${error.message}', error.source, error.offset); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: http_parser 2 | version: 4.1.0 3 | description: >- 4 | A platform-independent package for parsing and serializing HTTP formats. 5 | repository: https://github.com/dart-lang/http_parser 6 | 7 | environment: 8 | sdk: ^3.4.0 9 | 10 | dependencies: 11 | collection: ^1.19.0 12 | source_span: ^1.8.0 13 | string_scanner: ^1.1.0 14 | typed_data: ^1.3.0 15 | 16 | dev_dependencies: 17 | dart_flutter_team_lints: ^3.0.0 18 | test: ^1.16.6 19 | -------------------------------------------------------------------------------- /test/authentication_challenge_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 'package:http_parser/http_parser.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('parse', () { 10 | _singleChallengeTests(AuthenticationChallenge.parse); 11 | }); 12 | 13 | group('parseHeader', () { 14 | group('with a single challenge', () { 15 | _singleChallengeTests((challenge) { 16 | final challenges = AuthenticationChallenge.parseHeader(challenge); 17 | expect(challenges, hasLength(1)); 18 | return challenges.single; 19 | }); 20 | }); 21 | 22 | test('parses multiple challenges', () { 23 | final challenges = AuthenticationChallenge.parseHeader( 24 | 'scheme1 realm=fblthp, scheme2 realm=asdfg'); 25 | expect(challenges, hasLength(2)); 26 | expect(challenges.first.scheme, equals('scheme1')); 27 | expect(challenges.first.parameters, equals({'realm': 'fblthp'})); 28 | expect(challenges.last.scheme, equals('scheme2')); 29 | expect(challenges.last.parameters, equals({'realm': 'asdfg'})); 30 | }); 31 | 32 | test('parses multiple challenges with multiple parameters', () { 33 | final challenges = AuthenticationChallenge.parseHeader( 34 | 'scheme1 realm=fblthp, foo=bar, scheme2 realm=asdfg, baz=bang'); 35 | expect(challenges, hasLength(2)); 36 | 37 | expect(challenges.first.scheme, equals('scheme1')); 38 | expect(challenges.first.parameters, 39 | equals({'realm': 'fblthp', 'foo': 'bar'})); 40 | 41 | expect(challenges.last.scheme, equals('scheme2')); 42 | expect(challenges.last.parameters, 43 | equals({'realm': 'asdfg', 'baz': 'bang'})); 44 | }); 45 | }); 46 | } 47 | 48 | /// Tests to run for parsing a single challenge. 49 | /// 50 | /// These are run on both [AuthenticationChallenge.parse] and 51 | /// [AuthenticationChallenge.parseHeader], since they use almost entirely 52 | /// separate code paths. 53 | void _singleChallengeTests( 54 | AuthenticationChallenge Function(String challenge) parseChallenge) { 55 | test('parses a simple challenge', () { 56 | final challenge = parseChallenge('scheme realm=fblthp'); 57 | expect(challenge.scheme, equals('scheme')); 58 | expect(challenge.parameters, equals({'realm': 'fblthp'})); 59 | }); 60 | 61 | test('parses multiple parameters', () { 62 | final challenge = parseChallenge('scheme realm=fblthp, foo=bar, baz=qux'); 63 | expect(challenge.scheme, equals('scheme')); 64 | expect(challenge.parameters, 65 | equals({'realm': 'fblthp', 'foo': 'bar', 'baz': 'qux'})); 66 | }); 67 | 68 | test('parses quoted string parameters', () { 69 | final challenge = 70 | parseChallenge('scheme realm="fblthp, foo=bar", baz="qux"'); 71 | expect(challenge.scheme, equals('scheme')); 72 | expect(challenge.parameters, 73 | equals({'realm': 'fblthp, foo=bar', 'baz': 'qux'})); 74 | }); 75 | 76 | test('normalizes the case of the scheme', () { 77 | final challenge = parseChallenge('ScHeMe realm=fblthp'); 78 | expect(challenge.scheme, equals('scheme')); 79 | expect(challenge.parameters, equals({'realm': 'fblthp'})); 80 | }); 81 | 82 | test('normalizes the case of the parameter name', () { 83 | final challenge = parseChallenge('scheme ReAlM=fblthp'); 84 | expect(challenge.scheme, equals('scheme')); 85 | expect(challenge.parameters, containsPair('realm', 'fblthp')); 86 | }); 87 | 88 | test("doesn't normalize the case of the parameter value", () { 89 | final challenge = parseChallenge('scheme realm=FbLtHp'); 90 | expect(challenge.scheme, equals('scheme')); 91 | expect(challenge.parameters, containsPair('realm', 'FbLtHp')); 92 | expect(challenge.parameters, isNot(containsPair('realm', 'fblthp'))); 93 | }); 94 | 95 | test('allows extra whitespace', () { 96 | final challenge = parseChallenge( 97 | ' scheme\t \trealm\t = \tfblthp\t, \tfoo\t\r\n =\tbar\t'); 98 | expect(challenge.scheme, equals('scheme')); 99 | expect(challenge.parameters, equals({'realm': 'fblthp', 'foo': 'bar'})); 100 | }); 101 | 102 | test('allows an empty parameter', () { 103 | final challenge = parseChallenge('scheme realm=fblthp, , foo=bar'); 104 | expect(challenge.scheme, equals('scheme')); 105 | expect(challenge.parameters, equals({'realm': 'fblthp', 'foo': 'bar'})); 106 | }); 107 | 108 | test('allows a leading comma', () { 109 | final challenge = parseChallenge('scheme , realm=fblthp, foo=bar,'); 110 | expect(challenge.scheme, equals('scheme')); 111 | expect(challenge.parameters, equals({'realm': 'fblthp', 'foo': 'bar'})); 112 | }); 113 | 114 | test('allows a trailing comma', () { 115 | final challenge = parseChallenge('scheme realm=fblthp, foo=bar, ,'); 116 | expect(challenge.scheme, equals('scheme')); 117 | expect(challenge.parameters, equals({'realm': 'fblthp', 'foo': 'bar'})); 118 | }); 119 | 120 | test('disallows only a scheme', () { 121 | expect(() => parseChallenge('scheme'), throwsFormatException); 122 | }); 123 | 124 | test('disallows a valueless parameter', () { 125 | expect(() => parseChallenge('scheme realm'), throwsFormatException); 126 | expect(() => parseChallenge('scheme realm='), throwsFormatException); 127 | expect( 128 | () => parseChallenge('scheme realm, foo=bar'), throwsFormatException); 129 | }); 130 | 131 | test('requires a space after the scheme', () { 132 | expect(() => parseChallenge('scheme\trealm'), throwsFormatException); 133 | expect(() => parseChallenge('scheme\r\n\trealm='), throwsFormatException); 134 | }); 135 | 136 | test('disallows junk after the parameters', () { 137 | expect( 138 | () => parseChallenge('scheme realm=fblthp foo'), throwsFormatException); 139 | expect(() => parseChallenge('scheme realm=fblthp, foo=bar baz'), 140 | throwsFormatException); 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /test/case_insensitive_map_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 'package:http_parser/http_parser.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | test('provides case-insensitive access to the map', () { 10 | final map = CaseInsensitiveMap(); 11 | map['fOo'] = 'bAr'; 12 | expect(map, containsPair('FoO', 'bAr')); 13 | 14 | map['foo'] = 'baz'; 15 | expect(map, containsPair('FOO', 'baz')); 16 | }); 17 | 18 | test('stores the original key cases', () { 19 | final map = CaseInsensitiveMap(); 20 | map['fOo'] = 'bAr'; 21 | expect(map, equals({'fOo': 'bAr'})); 22 | }); 23 | 24 | test('.from() converts an existing map', () { 25 | final map = CaseInsensitiveMap.from({'fOo': 'bAr'}); 26 | expect(map, containsPair('FoO', 'bAr')); 27 | expect(map, equals({'fOo': 'bAr'})); 28 | }); 29 | 30 | test('.fromEntries() converts an existing map', () { 31 | final map = CaseInsensitiveMap.fromEntries({'fOo': 'bAr'}.entries); 32 | expect(map, containsPair('FoO', 'bAr')); 33 | expect(map, equals({'fOo': 'bAr'})); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/chunked_coding_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 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'package:http_parser/http_parser.dart'; 9 | import 'package:http_parser/src/chunked_coding/charcodes.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | group('encoder', () { 14 | test('adds a header to the chunk of bytes', () { 15 | expect(chunkedCoding.encode([1, 2, 3]), 16 | equals([$3, $cr, $lf, 1, 2, 3, $cr, $lf, $0, $cr, $lf, $cr, $lf])); 17 | }); 18 | 19 | test('uses hex for chunk size', () { 20 | final data = Iterable.generate(0xA7).toList(); 21 | expect( 22 | chunkedCoding.encode(data), 23 | equals( 24 | [$a, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf])); 25 | }); 26 | 27 | test('just generates a footer for an empty input', () { 28 | expect(chunkedCoding.encode([]), equals([$0, $cr, $lf, $cr, $lf])); 29 | }); 30 | 31 | group('with chunked conversion', () { 32 | late List> results; 33 | late ByteConversionSink sink; 34 | setUp(() { 35 | results = []; 36 | final controller = StreamController>(sync: true); 37 | controller.stream.listen(results.add); 38 | sink = chunkedCoding.encoder.startChunkedConversion(controller.sink); 39 | }); 40 | 41 | test('adds headers to each chunk of bytes', () { 42 | sink.add([1, 2, 3, 4]); 43 | expect( 44 | results, 45 | equals([ 46 | [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf] 47 | ])); 48 | 49 | sink.add([5, 6, 7]); 50 | expect( 51 | results, 52 | equals([ 53 | [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf], 54 | [$3, $cr, $lf, 5, 6, 7, $cr, $lf], 55 | ])); 56 | 57 | sink.close(); 58 | expect( 59 | results, 60 | equals([ 61 | [$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf], 62 | [$3, $cr, $lf, 5, 6, 7, $cr, $lf], 63 | [$0, $cr, $lf, $cr, $lf], 64 | ])); 65 | }); 66 | 67 | test('handles empty chunks', () { 68 | sink.add([]); 69 | expect(results, equals([[]])); 70 | 71 | sink.add([1, 2, 3]); 72 | expect( 73 | results, 74 | equals([ 75 | [], 76 | [$3, $cr, $lf, 1, 2, 3, $cr, $lf] 77 | ])); 78 | 79 | sink.add([]); 80 | expect( 81 | results, 82 | equals([ 83 | [], 84 | [$3, $cr, $lf, 1, 2, 3, $cr, $lf], 85 | [] 86 | ])); 87 | 88 | sink.close(); 89 | expect( 90 | results, 91 | equals([ 92 | [], 93 | [$3, $cr, $lf, 1, 2, 3, $cr, $lf], 94 | [], 95 | [$0, $cr, $lf, $cr, $lf], 96 | ])); 97 | }); 98 | 99 | group('addSlice()', () { 100 | test('adds bytes from the specified slice', () { 101 | sink.addSlice([1, 2, 3, 4, 5], 1, 4, false); 102 | expect( 103 | results, 104 | equals([ 105 | [$3, $cr, $lf, 2, 3, 4, $cr, $lf] 106 | ])); 107 | }); 108 | 109 | test("doesn't add a header if the slice is empty", () { 110 | sink.addSlice([1, 2, 3, 4, 5], 1, 1, false); 111 | expect(results, equals([[]])); 112 | }); 113 | 114 | test('adds a footer if isLast is true', () { 115 | sink.addSlice([1, 2, 3, 4, 5], 1, 4, true); 116 | expect( 117 | results, 118 | equals([ 119 | [$3, $cr, $lf, 2, 3, 4, $cr, $lf, $0, $cr, $lf, $cr, $lf] 120 | ])); 121 | 122 | // Setting isLast shuld close the sink. 123 | expect(() => sink.add([]), throwsStateError); 124 | }); 125 | 126 | group('disallows', () { 127 | test('start < 0', () { 128 | expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false), 129 | throwsRangeError); 130 | }); 131 | 132 | test('start > end', () { 133 | expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false), 134 | throwsRangeError); 135 | }); 136 | 137 | test('end > length', () { 138 | expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false), 139 | throwsRangeError); 140 | }); 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | group('decoder', () { 147 | test('parses chunked data', () { 148 | expect( 149 | chunkedCoding.decode([ 150 | $3, 151 | $cr, 152 | $lf, 153 | 1, 154 | 2, 155 | 3, 156 | $cr, 157 | $lf, 158 | $4, 159 | $cr, 160 | $lf, 161 | 4, 162 | 5, 163 | 6, 164 | 7, 165 | $cr, 166 | $lf, 167 | $0, 168 | $cr, 169 | $lf, 170 | $cr, 171 | $lf, 172 | ]), 173 | equals([1, 2, 3, 4, 5, 6, 7])); 174 | }); 175 | 176 | test('parses hex size', () { 177 | final data = Iterable.generate(0xA7).toList(); 178 | expect( 179 | chunkedCoding.decode( 180 | [$a, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf]), 181 | equals(data)); 182 | }); 183 | 184 | test('parses capital hex size', () { 185 | final data = Iterable.generate(0xA7).toList(); 186 | expect( 187 | chunkedCoding.decode( 188 | [$A, $7, $cr, $lf, ...data, $cr, $lf, $0, $cr, $lf, $cr, $lf]), 189 | equals(data)); 190 | }); 191 | 192 | test('parses an empty message', () { 193 | expect(chunkedCoding.decode([$0, $cr, $lf, $cr, $lf]), isEmpty); 194 | }); 195 | 196 | group('disallows a message', () { 197 | test('that ends without any input', () { 198 | expect(() => chunkedCoding.decode([]), throwsFormatException); 199 | }); 200 | 201 | test('that ends after the size', () { 202 | expect(() => chunkedCoding.decode([$a]), throwsFormatException); 203 | }); 204 | 205 | test('that ends after CR', () { 206 | expect(() => chunkedCoding.decode([$a, $cr]), throwsFormatException); 207 | }); 208 | 209 | test('that ends after LF', () { 210 | expect( 211 | () => chunkedCoding.decode([$a, $cr, $lf]), throwsFormatException); 212 | }); 213 | 214 | test('that ends after insufficient bytes', () { 215 | expect(() => chunkedCoding.decode([$a, $cr, $lf, 1, 2, 3]), 216 | throwsFormatException); 217 | }); 218 | 219 | test("that ends after a chunk's bytes", () { 220 | expect(() => chunkedCoding.decode([$1, $cr, $lf, 1]), 221 | throwsFormatException); 222 | }); 223 | 224 | test("that ends after a chunk's CR", () { 225 | expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr]), 226 | throwsFormatException); 227 | }); 228 | 229 | test("that ends atfter a chunk's LF", () { 230 | expect(() => chunkedCoding.decode([$1, $cr, $lf, 1, $cr, $lf]), 231 | throwsFormatException); 232 | }); 233 | 234 | test('that ends after the empty chunk', () { 235 | expect( 236 | () => chunkedCoding.decode([$0, $cr, $lf]), throwsFormatException); 237 | }); 238 | 239 | test('that ends after the closing CR', () { 240 | expect(() => chunkedCoding.decode([$0, $cr, $lf, $cr]), 241 | throwsFormatException); 242 | }); 243 | 244 | test('with a chunk without a size', () { 245 | expect(() => chunkedCoding.decode([$cr, $lf, $0, $cr, $lf, $cr, $lf]), 246 | throwsFormatException); 247 | }); 248 | 249 | test('with a chunk with a non-hex size', () { 250 | expect( 251 | () => chunkedCoding.decode([$q, $cr, $lf, $0, $cr, $lf, $cr, $lf]), 252 | throwsFormatException); 253 | }); 254 | }); 255 | 256 | group('with chunked conversion', () { 257 | late List> results; 258 | late ByteConversionSink sink; 259 | setUp(() { 260 | results = []; 261 | final controller = StreamController>(sync: true); 262 | controller.stream.listen(results.add); 263 | sink = chunkedCoding.decoder.startChunkedConversion(controller.sink); 264 | }); 265 | 266 | test('decodes each chunk of bytes', () { 267 | sink.add([$4, $cr, $lf, 1, 2, 3, 4, $cr, $lf]); 268 | expect( 269 | results, 270 | equals([ 271 | [1, 2, 3, 4] 272 | ])); 273 | 274 | sink.add([$3, $cr, $lf, 5, 6, 7, $cr, $lf]); 275 | expect( 276 | results, 277 | equals([ 278 | [1, 2, 3, 4], 279 | [5, 6, 7] 280 | ])); 281 | 282 | sink.add([$0, $cr, $lf, $cr, $lf]); 283 | sink.close(); 284 | expect( 285 | results, 286 | equals([ 287 | [1, 2, 3, 4], 288 | [5, 6, 7] 289 | ])); 290 | }); 291 | 292 | test('handles empty chunks', () { 293 | sink.add([]); 294 | expect(results, isEmpty); 295 | 296 | sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]); 297 | expect( 298 | results, 299 | equals([ 300 | [1, 2, 3] 301 | ])); 302 | 303 | sink.add([]); 304 | expect( 305 | results, 306 | equals([ 307 | [1, 2, 3] 308 | ])); 309 | 310 | sink.add([$0, $cr, $lf, $cr, $lf]); 311 | sink.close(); 312 | expect( 313 | results, 314 | equals([ 315 | [1, 2, 3] 316 | ])); 317 | }); 318 | 319 | test('throws if the sink is closed before the message is done', () { 320 | sink.add([$3, $cr, $lf, 1, 2, 3]); 321 | expect(() => sink.close(), throwsFormatException); 322 | }); 323 | 324 | group('preserves state when a byte array ends', () { 325 | test('within chunk size', () { 326 | sink.add([$a]); 327 | expect(results, isEmpty); 328 | 329 | final data = Iterable.generate(0xA7).toList(); 330 | sink.add([$7, $cr, $lf, ...data]); 331 | expect(results, equals([data])); 332 | }); 333 | 334 | test('after chunk size', () { 335 | sink.add([$3]); 336 | expect(results, isEmpty); 337 | 338 | sink.add([$cr, $lf, 1, 2, 3]); 339 | expect( 340 | results, 341 | equals([ 342 | [1, 2, 3] 343 | ])); 344 | }); 345 | 346 | test('after CR', () { 347 | sink.add([$3, $cr]); 348 | expect(results, isEmpty); 349 | 350 | sink.add([$lf, 1, 2, 3]); 351 | expect( 352 | results, 353 | equals([ 354 | [1, 2, 3] 355 | ])); 356 | }); 357 | 358 | test('after LF', () { 359 | sink.add([$3, $cr, $lf]); 360 | expect(results, isEmpty); 361 | 362 | sink.add([1, 2, 3]); 363 | expect( 364 | results, 365 | equals([ 366 | [1, 2, 3] 367 | ])); 368 | }); 369 | 370 | test('after some bytes', () { 371 | sink.add([$3, $cr, $lf, 1, 2]); 372 | expect( 373 | results, 374 | equals([ 375 | [1, 2] 376 | ])); 377 | 378 | sink.add([3]); 379 | expect( 380 | results, 381 | equals([ 382 | [1, 2], 383 | [3] 384 | ])); 385 | }); 386 | 387 | test('after all bytes', () { 388 | sink.add([$3, $cr, $lf, 1, 2, 3]); 389 | expect( 390 | results, 391 | equals([ 392 | [1, 2, 3] 393 | ])); 394 | 395 | sink.add([$cr, $lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]); 396 | expect( 397 | results, 398 | equals([ 399 | [1, 2, 3], 400 | [2, 3, 4] 401 | ])); 402 | }); 403 | 404 | test('after a post-chunk CR', () { 405 | sink.add([$3, $cr, $lf, 1, 2, 3, $cr]); 406 | expect( 407 | results, 408 | equals([ 409 | [1, 2, 3] 410 | ])); 411 | 412 | sink.add([$lf, $3, $cr, $lf, 2, 3, 4, $cr, $lf]); 413 | expect( 414 | results, 415 | equals([ 416 | [1, 2, 3], 417 | [2, 3, 4] 418 | ])); 419 | }); 420 | 421 | test('after a post-chunk LF', () { 422 | sink.add([$3, $cr, $lf, 1, 2, 3, $cr, $lf]); 423 | expect( 424 | results, 425 | equals([ 426 | [1, 2, 3] 427 | ])); 428 | 429 | sink.add([$3, $cr, $lf, 2, 3, 4, $cr, $lf]); 430 | expect( 431 | results, 432 | equals([ 433 | [1, 2, 3], 434 | [2, 3, 4] 435 | ])); 436 | }); 437 | 438 | test('after empty chunk size', () { 439 | sink.add([$0]); 440 | expect(results, isEmpty); 441 | 442 | sink.add([$cr, $lf, $cr, $lf]); 443 | expect(results, isEmpty); 444 | 445 | sink.close(); 446 | expect(results, isEmpty); 447 | }); 448 | 449 | test('after first empty chunk CR', () { 450 | sink.add([$0, $cr]); 451 | expect(results, isEmpty); 452 | 453 | sink.add([$lf, $cr, $lf]); 454 | expect(results, isEmpty); 455 | 456 | sink.close(); 457 | expect(results, isEmpty); 458 | }); 459 | 460 | test('after first empty chunk LF', () { 461 | sink.add([$0, $cr, $lf]); 462 | expect(results, isEmpty); 463 | 464 | sink.add([$cr, $lf]); 465 | expect(results, isEmpty); 466 | 467 | sink.close(); 468 | expect(results, isEmpty); 469 | }); 470 | 471 | test('after second empty chunk CR', () { 472 | sink.add([$0, $cr, $lf, $cr]); 473 | expect(results, isEmpty); 474 | 475 | sink.add([$lf]); 476 | expect(results, isEmpty); 477 | 478 | sink.close(); 479 | expect(results, isEmpty); 480 | }); 481 | }); 482 | 483 | group('addSlice()', () { 484 | test('adds bytes from the specified slice', () { 485 | sink.addSlice([1, $3, $cr, $lf, 2, 3, 4, 5], 1, 7, false); 486 | expect( 487 | results, 488 | equals([ 489 | [2, 3, 4] 490 | ])); 491 | }); 492 | 493 | test("doesn't decode if the slice is empty", () { 494 | sink.addSlice([1, 2, 3, 4, 5], 1, 1, false); 495 | expect(results, isEmpty); 496 | }); 497 | 498 | test('closes the sink if isLast is true', () { 499 | sink.addSlice([1, $0, $cr, $lf, $cr, $lf, 7], 1, 6, true); 500 | expect(results, isEmpty); 501 | }); 502 | 503 | group('disallows', () { 504 | test('start < 0', () { 505 | expect(() => sink.addSlice([1, 2, 3, 4, 5], -1, 4, false), 506 | throwsRangeError); 507 | }); 508 | 509 | test('start > end', () { 510 | expect(() => sink.addSlice([1, 2, 3, 4, 5], 3, 2, false), 511 | throwsRangeError); 512 | }); 513 | 514 | test('end > length', () { 515 | expect(() => sink.addSlice([1, 2, 3, 4, 5], 1, 10, false), 516 | throwsRangeError); 517 | }); 518 | }); 519 | }); 520 | }); 521 | }); 522 | } 523 | -------------------------------------------------------------------------------- /test/example_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('validate example', () { 7 | final result = Process.runSync( 8 | Platform.executable, 9 | [ 10 | '--enable-experiment=non-nullable', 11 | 'example/example.dart', 12 | ], 13 | ); 14 | 15 | expect(result.exitCode, 0); 16 | expect(result.stdout, ''' 17 | 2014-09-09 09:09:09.000Z 18 | Tue, 09 Sep 2014 09:09:09 GMT 19 | 2014-09-09 09:09:09.000Z 20 | '''); 21 | }, testOn: 'vm'); 22 | } 23 | -------------------------------------------------------------------------------- /test/http_date_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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:http_parser/http_parser.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('format', () { 10 | test('many values with 9', () { 11 | final date = DateTime.utc(2014, 9, 9, 9, 9, 9); 12 | final formatted = formatHttpDate(date); 13 | 14 | expect(formatted, 'Tue, 09 Sep 2014 09:09:09 GMT'); 15 | final parsed = parseHttpDate(formatted); 16 | 17 | expect(parsed, date); 18 | }); 19 | 20 | test('end of year', () { 21 | final date = DateTime.utc(1999, 12, 31, 23, 59, 59); 22 | final formatted = formatHttpDate(date); 23 | 24 | expect(formatted, 'Fri, 31 Dec 1999 23:59:59 GMT'); 25 | final parsed = parseHttpDate(formatted); 26 | 27 | expect(parsed, date); 28 | }); 29 | 30 | test('start of year', () { 31 | final date = DateTime.utc(2000); 32 | final formatted = formatHttpDate(date); 33 | 34 | expect(formatted, 'Sat, 01 Jan 2000 00:00:00 GMT'); 35 | final parsed = parseHttpDate(formatted); 36 | 37 | expect(parsed, date); 38 | }); 39 | }); 40 | 41 | group('parse', () { 42 | group('RFC 1123', () { 43 | test('parses the example date', () { 44 | final date = parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'); 45 | expect(date.day, equals(6)); 46 | expect(date.month, equals(DateTime.november)); 47 | expect(date.year, equals(1994)); 48 | expect(date.hour, equals(8)); 49 | expect(date.minute, equals(49)); 50 | expect(date.second, equals(37)); 51 | expect(date.timeZoneName, equals('UTC')); 52 | }); 53 | 54 | test('whitespace is required', () { 55 | expect(() => parseHttpDate('Sun,06 Nov 1994 08:49:37 GMT'), 56 | throwsFormatException); 57 | 58 | expect(() => parseHttpDate('Sun, 06Nov 1994 08:49:37 GMT'), 59 | throwsFormatException); 60 | 61 | expect(() => parseHttpDate('Sun, 06 Nov1994 08:49:37 GMT'), 62 | throwsFormatException); 63 | 64 | expect(() => parseHttpDate('Sun, 06 Nov 199408:49:37 GMT'), 65 | throwsFormatException); 66 | 67 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37GMT'), 68 | throwsFormatException); 69 | }); 70 | 71 | test('exactly one space is required', () { 72 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'), 73 | throwsFormatException); 74 | 75 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'), 76 | throwsFormatException); 77 | 78 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'), 79 | throwsFormatException); 80 | 81 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'), 82 | throwsFormatException); 83 | 84 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:37 GMT'), 85 | throwsFormatException); 86 | }); 87 | 88 | test('requires precise number lengths', () { 89 | expect(() => parseHttpDate('Sun, 6 Nov 1994 08:49:37 GMT'), 90 | throwsFormatException); 91 | 92 | expect(() => parseHttpDate('Sun, 06 Nov 94 08:49:37 GMT'), 93 | throwsFormatException); 94 | 95 | expect(() => parseHttpDate('Sun, 06 Nov 1994 8:49:37 GMT'), 96 | throwsFormatException); 97 | 98 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:9:37 GMT'), 99 | throwsFormatException); 100 | 101 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:7 GMT'), 102 | throwsFormatException); 103 | }); 104 | 105 | test('requires reasonable numbers', () { 106 | expect(() => parseHttpDate('Sun, 00 Nov 1994 08:49:37 GMT'), 107 | throwsFormatException); 108 | 109 | expect(() => parseHttpDate('Sun, 31 Nov 1994 08:49:37 GMT'), 110 | throwsFormatException); 111 | 112 | expect(() => parseHttpDate('Sun, 32 Aug 1994 08:49:37 GMT'), 113 | throwsFormatException); 114 | 115 | expect(() => parseHttpDate('Sun, 06 Nov 1994 24:49:37 GMT'), 116 | throwsFormatException); 117 | 118 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:60:37 GMT'), 119 | throwsFormatException); 120 | 121 | expect(() => parseHttpDate('Sun, 06 Nov 1994 08:49:60 GMT'), 122 | throwsFormatException); 123 | }); 124 | 125 | test('only allows short weekday names', () { 126 | expect(() => parseHttpDate('Sunday, 6 Nov 1994 08:49:37 GMT'), 127 | throwsFormatException); 128 | }); 129 | 130 | test('only allows short month names', () { 131 | expect(() => parseHttpDate('Sun, 6 November 1994 08:49:37 GMT'), 132 | throwsFormatException); 133 | }); 134 | 135 | test('only allows GMT', () { 136 | expect(() => parseHttpDate('Sun, 6 Nov 1994 08:49:37 PST'), 137 | throwsFormatException); 138 | }); 139 | 140 | test('disallows trailing whitespace', () { 141 | expect(() => parseHttpDate('Sun, 6 Nov 1994 08:49:37 GMT '), 142 | throwsFormatException); 143 | }); 144 | }); 145 | 146 | group('RFC 850', () { 147 | test('parses the example date', () { 148 | final date = parseHttpDate('Sunday, 06-Nov-94 08:49:37 GMT'); 149 | expect(date.day, equals(6)); 150 | expect(date.month, equals(DateTime.november)); 151 | expect(date.year, equals(1994)); 152 | expect(date.hour, equals(8)); 153 | expect(date.minute, equals(49)); 154 | expect(date.second, equals(37)); 155 | expect(date.timeZoneName, equals('UTC')); 156 | }); 157 | 158 | test('whitespace is required', () { 159 | expect(() => parseHttpDate('Sunday,06-Nov-94 08:49:37 GMT'), 160 | throwsFormatException); 161 | 162 | expect(() => parseHttpDate('Sunday, 06-Nov-9408:49:37 GMT'), 163 | throwsFormatException); 164 | 165 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:37GMT'), 166 | throwsFormatException); 167 | }); 168 | 169 | test('exactly one space is required', () { 170 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:37 GMT'), 171 | throwsFormatException); 172 | 173 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:37 GMT'), 174 | throwsFormatException); 175 | 176 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:37 GMT'), 177 | throwsFormatException); 178 | }); 179 | 180 | test('requires precise number lengths', () { 181 | expect(() => parseHttpDate('Sunday, 6-Nov-94 08:49:37 GMT'), 182 | throwsFormatException); 183 | 184 | expect(() => parseHttpDate('Sunday, 06-Nov-1994 08:49:37 GMT'), 185 | throwsFormatException); 186 | 187 | expect(() => parseHttpDate('Sunday, 06-Nov-94 8:49:37 GMT'), 188 | throwsFormatException); 189 | 190 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:9:37 GMT'), 191 | throwsFormatException); 192 | 193 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:7 GMT'), 194 | throwsFormatException); 195 | }); 196 | 197 | test('requires reasonable numbers', () { 198 | expect(() => parseHttpDate('Sunday, 00-Nov-94 08:49:37 GMT'), 199 | throwsFormatException); 200 | 201 | expect(() => parseHttpDate('Sunday, 31-Nov-94 08:49:37 GMT'), 202 | throwsFormatException); 203 | 204 | expect(() => parseHttpDate('Sunday, 32-Aug-94 08:49:37 GMT'), 205 | throwsFormatException); 206 | 207 | expect(() => parseHttpDate('Sunday, 06-Nov-94 24:49:37 GMT'), 208 | throwsFormatException); 209 | 210 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:60:37 GMT'), 211 | throwsFormatException); 212 | 213 | expect(() => parseHttpDate('Sunday, 06-Nov-94 08:49:60 GMT'), 214 | throwsFormatException); 215 | }); 216 | 217 | test('only allows long weekday names', () { 218 | expect(() => parseHttpDate('Sun, 6-Nov-94 08:49:37 GMT'), 219 | throwsFormatException); 220 | }); 221 | 222 | test('only allows short month names', () { 223 | expect(() => parseHttpDate('Sunday, 6-November-94 08:49:37 GMT'), 224 | throwsFormatException); 225 | }); 226 | 227 | test('only allows GMT', () { 228 | expect(() => parseHttpDate('Sunday, 6-Nov-94 08:49:37 PST'), 229 | throwsFormatException); 230 | }); 231 | 232 | test('disallows trailing whitespace', () { 233 | expect(() => parseHttpDate('Sunday, 6-Nov-94 08:49:37 GMT '), 234 | throwsFormatException); 235 | }); 236 | }); 237 | 238 | group('asctime()', () { 239 | test('parses the example date', () { 240 | final date = parseHttpDate('Sun Nov 6 08:49:37 1994'); 241 | expect(date.day, equals(6)); 242 | expect(date.month, equals(DateTime.november)); 243 | expect(date.year, equals(1994)); 244 | expect(date.hour, equals(8)); 245 | expect(date.minute, equals(49)); 246 | expect(date.second, equals(37)); 247 | expect(date.timeZoneName, equals('UTC')); 248 | }); 249 | 250 | test('parses a date with a two-digit day', () { 251 | final date = parseHttpDate('Sun Nov 16 08:49:37 1994'); 252 | expect(date.day, equals(16)); 253 | expect(date.month, equals(DateTime.november)); 254 | expect(date.year, equals(1994)); 255 | expect(date.hour, equals(8)); 256 | expect(date.minute, equals(49)); 257 | expect(date.second, equals(37)); 258 | expect(date.timeZoneName, equals('UTC')); 259 | }); 260 | 261 | test('whitespace is required', () { 262 | expect(() => parseHttpDate('SunNov 6 08:49:37 1994'), 263 | throwsFormatException); 264 | 265 | expect(() => parseHttpDate('Sun Nov6 08:49:37 1994'), 266 | throwsFormatException); 267 | 268 | expect(() => parseHttpDate('Sun Nov 608:49:37 1994'), 269 | throwsFormatException); 270 | 271 | expect(() => parseHttpDate('Sun Nov 6 08:49:371994'), 272 | throwsFormatException); 273 | }); 274 | 275 | test('the right amount of whitespace is required', () { 276 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 1994'), 277 | throwsFormatException); 278 | 279 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 1994'), 280 | throwsFormatException); 281 | 282 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 1994'), 283 | throwsFormatException); 284 | 285 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 1994'), 286 | throwsFormatException); 287 | 288 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 1994'), 289 | throwsFormatException); 290 | }); 291 | 292 | test('requires precise number lengths', () { 293 | expect(() => parseHttpDate('Sun Nov 016 08:49:37 1994'), 294 | throwsFormatException); 295 | 296 | expect(() => parseHttpDate('Sun Nov 6 8:49:37 1994'), 297 | throwsFormatException); 298 | 299 | expect(() => parseHttpDate('Sun Nov 6 08:9:37 1994'), 300 | throwsFormatException); 301 | 302 | expect(() => parseHttpDate('Sun Nov 6 08:49:7 1994'), 303 | throwsFormatException); 304 | 305 | expect(() => parseHttpDate('Sun Nov 6 08:49:37 94'), 306 | throwsFormatException); 307 | }); 308 | 309 | test('requires reasonable numbers', () { 310 | expect(() => parseHttpDate('Sun Nov 0 08:49:37 1994'), 311 | throwsFormatException); 312 | 313 | expect(() => parseHttpDate('Sun Nov 31 08:49:37 1994'), 314 | throwsFormatException); 315 | 316 | expect(() => parseHttpDate('Sun Aug 32 08:49:37 1994'), 317 | throwsFormatException); 318 | 319 | expect(() => parseHttpDate('Sun Nov 6 24:49:37 1994'), 320 | throwsFormatException); 321 | 322 | expect(() => parseHttpDate('Sun Nov 6 08:60:37 1994'), 323 | throwsFormatException); 324 | 325 | expect(() => parseHttpDate('Sun Nov 6 08:49:60 1994'), 326 | throwsFormatException); 327 | }); 328 | 329 | test('only allows short weekday names', () { 330 | expect(() => parseHttpDate('Sunday Nov 0 08:49:37 1994'), 331 | throwsFormatException); 332 | }); 333 | 334 | test('only allows short month names', () { 335 | expect(() => parseHttpDate('Sun November 0 08:49:37 1994'), 336 | throwsFormatException); 337 | }); 338 | 339 | test('disallows trailing whitespace', () { 340 | expect(() => parseHttpDate('Sun November 0 08:49:37 1994 '), 341 | throwsFormatException); 342 | }); 343 | }); 344 | }); 345 | } 346 | -------------------------------------------------------------------------------- /test/media_type_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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:http_parser/http_parser.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('parse', () { 10 | test('parses a simple MIME type', () { 11 | final type = MediaType.parse('text/plain'); 12 | expect(type.type, equals('text')); 13 | expect(type.subtype, equals('plain')); 14 | }); 15 | 16 | test('allows leading whitespace', () { 17 | expect(MediaType.parse(' text/plain').mimeType, equals('text/plain')); 18 | expect(MediaType.parse('\ttext/plain').mimeType, equals('text/plain')); 19 | }); 20 | 21 | test('allows trailing whitespace', () { 22 | expect(MediaType.parse('text/plain ').mimeType, equals('text/plain')); 23 | expect(MediaType.parse('text/plain\t').mimeType, equals('text/plain')); 24 | }); 25 | 26 | test('disallows separators in the MIME type', () { 27 | expect(() => MediaType.parse('te(xt/plain'), throwsFormatException); 28 | expect(() => MediaType.parse('text/pla=in'), throwsFormatException); 29 | }); 30 | 31 | test('disallows whitespace around the slash', () { 32 | expect(() => MediaType.parse('text /plain'), throwsFormatException); 33 | expect(() => MediaType.parse('text/ plain'), throwsFormatException); 34 | }); 35 | 36 | test('parses parameters', () { 37 | final type = MediaType.parse('text/plain;foo=bar;baz=bang'); 38 | expect(type.mimeType, equals('text/plain')); 39 | expect(type.parameters, equals({'foo': 'bar', 'baz': 'bang'})); 40 | }); 41 | 42 | test('allows whitespace around the semicolon', () { 43 | final type = MediaType.parse('text/plain ; foo=bar ; baz=bang'); 44 | expect(type.mimeType, equals('text/plain')); 45 | expect(type.parameters, equals({'foo': 'bar', 'baz': 'bang'})); 46 | }); 47 | 48 | test('disallows whitespace around the equals', () { 49 | expect( 50 | () => MediaType.parse('text/plain; foo =bar'), throwsFormatException); 51 | expect( 52 | () => MediaType.parse('text/plain; foo= bar'), throwsFormatException); 53 | }); 54 | 55 | test('disallows separators in the parameters', () { 56 | expect( 57 | () => MediaType.parse('text/plain; fo:o=bar'), throwsFormatException); 58 | expect( 59 | () => MediaType.parse('text/plain; foo=b@ar'), throwsFormatException); 60 | }); 61 | 62 | test('parses quoted parameters', () { 63 | final type = 64 | MediaType.parse(r'text/plain; foo="bar space"; baz="bang\\escape"'); 65 | expect(type.mimeType, equals('text/plain')); 66 | expect( 67 | type.parameters, equals({'foo': 'bar space', 'baz': r'bang\escape'})); 68 | }); 69 | 70 | test('lower-cases type and subtype', () { 71 | final type = MediaType.parse('TeXt/pLaIn'); 72 | expect(type.type, equals('text')); 73 | expect(type.subtype, equals('plain')); 74 | expect(type.mimeType, equals('text/plain')); 75 | }); 76 | 77 | test('records parameters as case-insensitive', () { 78 | final type = MediaType.parse('test/plain;FoO=bar;bAz=bang'); 79 | expect(type.parameters, equals({'FoO': 'bar', 'bAz': 'bang'})); 80 | expect(type.parameters, containsPair('foo', 'bar')); 81 | expect(type.parameters, containsPair('baz', 'bang')); 82 | }); 83 | }); 84 | 85 | group('change', () { 86 | late MediaType type; 87 | setUp(() { 88 | type = MediaType.parse('text/plain; foo=bar; baz=bang'); 89 | }); 90 | 91 | test('uses the existing fields by default', () { 92 | final newType = type.change(); 93 | expect(newType.type, equals('text')); 94 | expect(newType.subtype, equals('plain')); 95 | expect(newType.parameters, equals({'foo': 'bar', 'baz': 'bang'})); 96 | }); 97 | 98 | test('[type] overrides the existing type', () { 99 | expect(type.change(type: 'new').type, equals('new')); 100 | }); 101 | 102 | test('[subtype] overrides the existing subtype', () { 103 | expect(type.change(subtype: 'new').subtype, equals('new')); 104 | }); 105 | 106 | test('[mimeType] overrides the existing type and subtype', () { 107 | final newType = type.change(mimeType: 'image/png'); 108 | expect(newType.type, equals('image')); 109 | expect(newType.subtype, equals('png')); 110 | }); 111 | 112 | test('[parameters] overrides and adds to existing parameters', () { 113 | expect( 114 | type.change(parameters: {'foo': 'zap', 'qux': 'fblthp'}).parameters, 115 | equals({'foo': 'zap', 'baz': 'bang', 'qux': 'fblthp'})); 116 | }); 117 | 118 | test('[clearParameters] removes existing parameters', () { 119 | expect(type.change(clearParameters: true).parameters, isEmpty); 120 | }); 121 | 122 | test('[clearParameters] with [parameters] removes before adding', () { 123 | final newType = 124 | type.change(parameters: {'foo': 'zap'}, clearParameters: true); 125 | expect(newType.parameters, equals({'foo': 'zap'})); 126 | }); 127 | 128 | test('[type] with [mimeType] is illegal', () { 129 | expect(() => type.change(type: 'new', mimeType: 'image/png'), 130 | throwsArgumentError); 131 | }); 132 | 133 | test('[subtype] with [mimeType] is illegal', () { 134 | expect(() => type.change(subtype: 'new', mimeType: 'image/png'), 135 | throwsArgumentError); 136 | }); 137 | }); 138 | 139 | group('toString', () { 140 | test('serializes a simple MIME type', () { 141 | expect(MediaType('text', 'plain').toString(), equals('text/plain')); 142 | }); 143 | 144 | test('serializes a token parameter as a token', () { 145 | expect(MediaType('text', 'plain', {'foo': 'bar'}).toString(), 146 | equals('text/plain; foo=bar')); 147 | }); 148 | 149 | test('serializes a non-token parameter as a quoted string', () { 150 | expect(MediaType('text', 'plain', {'foo': 'bar baz'}).toString(), 151 | equals('text/plain; foo="bar baz"')); 152 | }); 153 | 154 | test('escapes a quoted string as necessary', () { 155 | expect(MediaType('text', 'plain', {'foo': 'bar"\x7Fbaz'}).toString(), 156 | equals('text/plain; foo="bar\\"\\\x7Fbaz"')); 157 | }); 158 | 159 | test('serializes multiple parameters', () { 160 | expect( 161 | MediaType('text', 'plain', {'foo': 'bar', 'baz': 'bang'}).toString(), 162 | equals('text/plain; foo=bar; baz=bang')); 163 | }); 164 | }); 165 | } 166 | --------------------------------------------------------------------------------