diff --git a/pkgs/mime/.github/dependabot.yaml b/pkgs/mime/.github/dependabot.yaml new file mode 100644 index 00000000..bf6b38a4 --- /dev/null +++ b/pkgs/mime/.github/dependabot.yaml @@ -0,0 +1,14 @@ +# Dependabot configuration file. +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - autosubmit + groups: + github-actions: + patterns: + - "*" diff --git a/pkgs/mime/.github/workflows/no-response.yml b/pkgs/mime/.github/workflows/no-response.yml new file mode 100644 index 00000000..ab1ac498 --- /dev/null +++ b/pkgs/mime/.github/workflows/no-response.yml @@ -0,0 +1,37 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + with: + # Don't automatically mark inactive issues+PRs as stale. + days-before-stale: -1 + # Close needs-info issues and PRs after 14 days of inactivity. + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution! diff --git a/pkgs/mime/.github/workflows/publish.yaml b/pkgs/mime/.github/workflows/publish.yaml new file mode 100644 index 00000000..27157a04 --- /dev/null +++ b/pkgs/mime/.github/workflows/publish.yaml @@ -0,0 +1,17 @@ +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ master ] + push: + tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'dart-lang' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + permissions: + id-token: write # Required for authentication using OIDC + pull-requests: write # Required for writing the pull request note diff --git a/pkgs/mime/.github/workflows/test-package.yml b/pkgs/mime/.github/workflows/test-package.yml new file mode 100644 index 00000000..13b1dd51 --- /dev/null +++ b/pkgs/mime/.github/workflows/test-package.yml @@ -0,0 +1,36 @@ +name: Dart CI + +on: + schedule: + # “At 00:00 (UTC) on Sunday.” + - cron: '0 0 * * 0' + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + os: [ubuntu-latest] + sdk: [3.2, dev] + steps: + # These are the latest versions of the github actions; dependabot will + # send PRs to keep these up-to-date. + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 + + - name: Install dependencies + run: dart pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze project source + run: dart analyze --fatal-infos + + - name: Run tests + run: dart test diff --git a/pkgs/mime/.gitignore b/pkgs/mime/.gitignore new file mode 100644 index 00000000..a433102c --- /dev/null +++ b/pkgs/mime/.gitignore @@ -0,0 +1,5 @@ +.packages +.dart_tool/ +.pub/ +packages +pubspec.lock diff --git a/pkgs/mime/AUTHORS b/pkgs/mime/AUTHORS new file mode 100644 index 00000000..e8063a8c --- /dev/null +++ b/pkgs/mime/AUTHORS @@ -0,0 +1,6 @@ +# Below is a list of people and organizations that have contributed +# to the project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. diff --git a/pkgs/mime/CHANGELOG.md b/pkgs/mime/CHANGELOG.md new file mode 100644 index 00000000..347e55d3 --- /dev/null +++ b/pkgs/mime/CHANGELOG.md @@ -0,0 +1,104 @@ +## 1.0.6-wip + +* Add `topics` section to `pubspec.yaml`. + +## 1.0.5 + +* Update `video/mp4` mimeType lookup by header bytes. +* Add `image/heic` mimeType lookup by header bytes. +* Add `image/heif` mimeType lookup by header bytes. +* Add m4b mimeType lookup by extension. +* Add `text/markdown` mimeType lookup by extension. +* Require Dart 3.2.0. + +## 1.0.4 + +* Changed `.js` to `text/javascript` per + https://datatracker.ietf.org/doc/html/rfc9239. +* Added `.mjs` as `text/javascript`. +* Add `application/dicom` mimeType lookup by extension. +* Require Dart 2.18. + +## 1.0.3 + +* Add application/manifest+json lookup by extension. +* Add application/toml mimeType lookup by extension. +* Add audio/aac mimeType lookup by header bytes. +* Add audio/mpeg mimeType lookup by header bytes. +* Add audio/ogg mimeType lookup by header bytes. +* Add audio/weba mimeType lookup by header bytes. +* Add font/woff2 lookup by extension and header bytes. +* Add image/avif mimeType lookup by extension. +* Add image/heic mimeType lookup by extension. +* Add image/heif mimeType lookup by extension. +* Change audio/x-aac to audio/aac when detected by extension. + +## 1.0.2 + +* Add audio/x-aiff mimeType lookup by header bytes. +* Add audio/x-flac mimeType lookup by header bytes. +* Add audio/x-wav mimeType lookup by header bytes. +* Add audio/mp4 mimeType lookup by file path. + +## 1.0.1 + +* Add image/webp mimeType lookup by header bytes. + +## 1.0.0 + +* Stable null safety release. + +## 1.0.0-nullsafety.0 + +* Update to null safety. + +## 0.9.7 + +* Add `extensionFromMime` utility function. + +## 0.9.6+3 + +* Change the mime type for Dart source from `application/dart` to `text/x-dart`. +* Add example. +* Fix links and code in README. + +## 0.9.6+2 + +* Set max SDK version to `<3.0.0`, and adjust other dependencies. + +## 0.9.6+1 + +* Stop using deprecated constants from the SDK. + +## 0.9.6 + +* Updates to support Dart 2.0 core library changes (wave + 2.2). See [issue 31847][sdk#31847] for details. + + [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847 + +## 0.9.5 + +* Add support for the WebAssembly format. + +## 0.9.4 + +* Updated Dart SDK requirement to `>= 1.8.3 <2.0.0` + +* Strong-mode clean. + +* Added support for glTF text and binary formats. + +## 0.9.3 + +* Fixed erroneous behavior for listening and when pausing/resuming + stream of parts. + +## 0.9.2 + +* Fixed erroneous behavior when pausing/canceling stream of parts but already + listened to one part. + +## 0.9.1 + +* Handle parsing of MIME multipart content with no parts. diff --git a/pkgs/mime/CONTRIBUTING.md b/pkgs/mime/CONTRIBUTING.md new file mode 100644 index 00000000..6f5e0ea6 --- /dev/null +++ b/pkgs/mime/CONTRIBUTING.md @@ -0,0 +1,33 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. + +### File headers +All files in the project must start with the following header. + + // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file + // for details. All rights reserved. Use of this source code is governed by a + // BSD-style license that can be found in the LICENSE file. + +### The small print +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). diff --git a/pkgs/mime/LICENSE b/pkgs/mime/LICENSE new file mode 100644 index 00000000..dbd2843a --- /dev/null +++ b/pkgs/mime/LICENSE @@ -0,0 +1,27 @@ +Copyright 2015, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/mime/README.md b/pkgs/mime/README.md new file mode 100644 index 00000000..4cd060ce --- /dev/null +++ b/pkgs/mime/README.md @@ -0,0 +1,59 @@ +[![Build Status](https://github.com/dart-lang/mime/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/mime/actions?query=workflow%3A"Dart+CI"+branch%3Amaster) +[![Pub Package](https://img.shields.io/pub/v/mime.svg)](https://pub.dev/packages/mime) +[![package publisher](https://img.shields.io/pub/publisher/mime.svg)](https://pub.dev/packages/mime/publisher) + +Package for working with MIME type definitions and for processing +streams of MIME multipart media types. + +## Determining the MIME type for a file + +The `MimeTypeResolver` class can be used to determine the MIME type of +a file. It supports both using the extension of the file name and +looking at magic bytes from the beginning of the file. + +There is a builtin instance of `MimeTypeResolver` accessible through +the top level function `lookupMimeType`. This builtin instance has +the most common file name extensions and magic bytes registered. + +```dart +import 'package:mime/mime.dart'; + +void main() { + print(lookupMimeType('test.html')); + // text/html + + print(lookupMimeType('test', headerBytes: [0xFF, 0xD8])); + // image/jpeg + + print(lookupMimeType('test.html', headerBytes: [0xFF, 0xD8])); + // image/jpeg +} +``` + +You can build you own resolver by creating an instance of +`MimeTypeResolver` and adding file name extensions and magic bytes +using `addExtension` and `addMagicNumber`. + +## Processing MIME multipart media types + +The class `MimeMultipartTransformer` is used to process a `Stream` of +bytes encoded using a MIME multipart media types encoding. The +transformer provides a new `Stream` of `MimeMultipart` objects each of +which have the headers and the content of each part. The content of a +part is provided as a stream of bytes. + +Below is an example showing how to process an HTTP request and print +the length of the content of each part. + +```dart +// HTTP request with content type multipart/form-data. +HttpRequest request = ...; +// Determine the boundary form the content type header +String boundary = request.headers.contentType.parameters['boundary']; + +// Process the body just calculating the length of each part. +request + .transform(new MimeMultipartTransformer(boundary)) + .map((part) => part.fold(0, (p, d) => p + d)) + .listen((length) => print('Part with length $length')); +``` diff --git a/pkgs/mime/analysis_options.yaml b/pkgs/mime/analysis_options.yaml new file mode 100644 index 00000000..44b61cfd --- /dev/null +++ b/pkgs/mime/analysis_options.yaml @@ -0,0 +1,34 @@ +# https://dart.dev/tools/analysis#the-analysis-options-file +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - join_return_with_assignment + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - package_api_docs + - prefer_const_declarations + - prefer_expression_function_bodies + - prefer_final_locals + - unnecessary_await_in_return + - unnecessary_breaks + - unnecessary_raw_strings + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers diff --git a/pkgs/mime/example/example.dart b/pkgs/mime/example/example.dart new file mode 100644 index 00000000..af14cb08 --- /dev/null +++ b/pkgs/mime/example/example.dart @@ -0,0 +1,12 @@ +import 'package:mime/mime.dart'; + +void main() { + print(lookupMimeType('test.html')); + // text/html + + print(lookupMimeType('test', headerBytes: [0xFF, 0xD8])); + // image/jpeg + + print(lookupMimeType('test.html', headerBytes: [0xFF, 0xD8])); + // image/jpeg +} diff --git a/pkgs/mime/lib/mime.dart b/pkgs/mime/lib/mime.dart new file mode 100644 index 00000000..f2c44640 --- /dev/null +++ b/pkgs/mime/lib/mime.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Help for working with file format identifiers +/// such as `text/html` and `image/png`. +/// +/// More details, including a list of types, are in the Wikipedia article +/// [Internet media type](http://en.wikipedia.org/wiki/Internet_media_type). +library; + +export 'src/mime_multipart_transformer.dart'; +export 'src/mime_shared.dart'; +export 'src/mime_type.dart'; diff --git a/pkgs/mime/lib/src/bound_multipart_stream.dart b/pkgs/mime/lib/src/bound_multipart_stream.dart new file mode 100644 index 00000000..cc01fd5b --- /dev/null +++ b/pkgs/mime/lib/src/bound_multipart_stream.dart @@ -0,0 +1,373 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'char_code.dart' as char_code; +import 'mime_shared.dart'; + +/// Bytes for '()<>@,;:\\"/[]?={} \t'. +const _separators = { + 40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, 91, 93, 63, 61, 123, 125, 32, 9 // +}; + +bool _isTokenChar(int byte) => + byte > 31 && byte < 128 && !_separators.contains(byte); + +int _toLowerCase(int byte) { + const delta = char_code.lowerA - char_code.upperA; + return (char_code.upperA <= byte && byte <= char_code.upperZ) + ? byte + delta + : byte; +} + +void _expectByteValue(int val1, int val2) { + if (val1 != val2) { + throw const MimeMultipartException('Failed to parse multipart mime 1'); + } +} + +void _expectWhitespace(int byte) { + if (byte != char_code.sp && byte != char_code.ht) { + throw const MimeMultipartException('Failed to parse multipart mime 2'); + } +} + +class _MimeMultipart extends MimeMultipart { + @override + final Map headers; + final Stream> _stream; + + _MimeMultipart(this.headers, this._stream); + + @override + StreamSubscription> listen( + void Function(List data)? onData, { + void Function()? onDone, + Function? onError, + bool? cancelOnError, + }) => + _stream.listen( + onData, + onDone: onDone, + onError: onError, + cancelOnError: cancelOnError, + ); +} + +class BoundMultipartStream { + static const int _startCode = 0; + static const int _boundaryEndingCode = 1; + static const int _boundaryEndCode = 2; + static const int _headerStartCode = 3; + static const int _headerFieldCode = 4; + static const int _headerValueStartCode = 5; + static const int _headerValueCode = 6; + static const int _headerValueFoldingOrEndingCode = 7; + static const int _headerValueFoldOrEndCode = 8; + static const int _headerEndingCode = 9; + static const int _contentCode = 10; + static const int _lastBoundaryDash2Code = 11; + static const int _lastBoundaryEndingCode = 12; + static const int _lastBoundaryEndCode = 13; + static const int _doneCode = 14; + static const int _failCode = 15; + + final List _boundary; + final List _headerField = []; + final List _headerValue = []; + + // The following states belong to `_controller`, state changes will not be + // immediately acted upon but rather only after the current + // `_multipartController` is done. + static const int _controllerStateIdle = 0; + static const int _controllerStateActive = 1; + static const int _controllerStatePaused = 2; + static const int _controllerStateCanceled = 3; + + int _controllerState = _controllerStateIdle; + + final _controller = StreamController(sync: true); + + Stream get stream => _controller.stream; + + late StreamSubscription _subscription; + + StreamController>? _multipartController; + Map? _headers; + + int _state = _startCode; + int _boundaryIndex = 2; + + /// Current index into [_buffer]. + /// + /// If index is negative then it is the index into the artificial prefix of + /// the boundary string. + int _index = 0; + List _buffer = _placeholderBuffer; + + BoundMultipartStream(this._boundary, Stream> stream) { + _controller + ..onPause = _pauseStream + ..onResume = _resumeStream + ..onCancel = () { + _controllerState = _controllerStateCanceled; + _tryPropagateControllerState(); + } + ..onListen = () { + _controllerState = _controllerStateActive; + _subscription = stream.listen((data) { + assert(_buffer == _placeholderBuffer); + _subscription.pause(); + _buffer = data; + _index = 0; + _parse(); + }, onDone: () { + if (_state != _doneCode) { + _controller + .addError(const MimeMultipartException('Bad multipart ending')); + } + _controller.close(); + }, onError: _controller.addError); + }; + } + + void _resumeStream() { + assert(_controllerState == _controllerStatePaused); + _controllerState = _controllerStateActive; + _tryPropagateControllerState(); + } + + void _pauseStream() { + _controllerState = _controllerStatePaused; + _tryPropagateControllerState(); + } + + void _tryPropagateControllerState() { + if (_multipartController == null) { + switch (_controllerState) { + case _controllerStateActive: + if (_subscription.isPaused) _subscription.resume(); + case _controllerStatePaused: + if (!_subscription.isPaused) _subscription.pause(); + case _controllerStateCanceled: + _subscription.cancel(); + default: + throw StateError('This code should never be reached.'); + } + } + } + + void _parse() { + // Number of boundary bytes to artificially place before the supplied data. + // The data to parse might be 'artificially' prefixed with a + // partial match of the boundary. + final boundaryPrefix = _boundaryIndex; + // Position where content starts. Will be null if no known content + // start exists. Will be negative of the content starts in the + // boundary prefix. Will be zero or position if the content starts + // in the current buffer. + var contentStartIndex = + _state == _contentCode && _boundaryIndex == 0 ? 0 : null; + + // Function to report content data for the current part. The data + // reported is from the current content start index up til the + // current index. As the data can be artificially prefixed with a + // prefix of the boundary both the content start index and index + // can be negative. + void reportData() { + if (contentStartIndex! < 0) { + final contentLength = boundaryPrefix + _index - _boundaryIndex; + if (contentLength <= boundaryPrefix) { + _multipartController!.add(_boundary.sublist(0, contentLength)); + } else { + _multipartController!.add(_boundary.sublist(0, boundaryPrefix)); + _multipartController! + .add(_buffer.sublist(0, contentLength - boundaryPrefix)); + } + } else { + final contentEndIndex = _index - _boundaryIndex; + _multipartController! + .add(_buffer.sublist(contentStartIndex, contentEndIndex)); + } + } + + while ( + _index < _buffer.length && _state != _failCode && _state != _doneCode) { + final byte = + _index < 0 ? _boundary[boundaryPrefix + _index] : _buffer[_index]; + switch (_state) { + case _startCode: + if (byte == _boundary[_boundaryIndex]) { + _boundaryIndex++; + if (_boundaryIndex == _boundary.length) { + _state = _boundaryEndingCode; + _boundaryIndex = 0; + } + } else { + // Restart matching of the boundary. + _index = _index - _boundaryIndex; + _boundaryIndex = 0; + } + + case _boundaryEndingCode: + if (byte == char_code.cr) { + _state = _boundaryEndCode; + } else if (byte == char_code.dash) { + _state = _lastBoundaryDash2Code; + } else { + _expectWhitespace(byte); + } + + case _boundaryEndCode: + _expectByteValue(byte, char_code.lf); + _multipartController?.close(); + if (_multipartController != null) { + _multipartController = null; + _tryPropagateControllerState(); + } + _state = _headerStartCode; + + case _headerStartCode: + _headers = {}; + if (byte == char_code.cr) { + _state = _headerEndingCode; + } else { + // Start of new header field. + _headerField.add(_toLowerCase(byte)); + _state = _headerFieldCode; + } + + case _headerFieldCode: + if (byte == char_code.colon) { + _state = _headerValueStartCode; + } else { + if (!_isTokenChar(byte)) { + throw const MimeMultipartException('Invalid header field name'); + } + _headerField.add(_toLowerCase(byte)); + } + + case _headerValueStartCode: + if (byte == char_code.cr) { + _state = _headerValueFoldingOrEndingCode; + } else if (byte != char_code.sp && byte != char_code.ht) { + // Start of new header value. + _headerValue.add(byte); + _state = _headerValueCode; + } + + case _headerValueCode: + if (byte == char_code.cr) { + _state = _headerValueFoldingOrEndingCode; + } else { + _headerValue.add(byte); + } + + case _headerValueFoldingOrEndingCode: + _expectByteValue(byte, char_code.lf); + _state = _headerValueFoldOrEndCode; + + case _headerValueFoldOrEndCode: + if (byte == char_code.sp || byte == char_code.ht) { + _state = _headerValueStartCode; + } else { + final headerField = utf8.decode(_headerField); + final headerValue = utf8.decode(_headerValue); + _headers![headerField.toLowerCase()] = headerValue; + _headerField.clear(); + _headerValue.clear(); + if (byte == char_code.cr) { + _state = _headerEndingCode; + } else { + // Start of new header field. + _headerField.add(_toLowerCase(byte)); + _state = _headerFieldCode; + } + } + + case _headerEndingCode: + _expectByteValue(byte, char_code.lf); + _multipartController = StreamController( + sync: true, + onListen: () { + if (_subscription.isPaused) _subscription.resume(); + }, + onPause: _subscription.pause, + onResume: _subscription.resume); + _controller + .add(_MimeMultipart(_headers!, _multipartController!.stream)); + _headers = null; + _state = _contentCode; + contentStartIndex = _index + 1; + + case _contentCode: + if (byte == _boundary[_boundaryIndex]) { + _boundaryIndex++; + if (_boundaryIndex == _boundary.length) { + if (contentStartIndex != null) { + _index++; + reportData(); + _index--; + } + _multipartController!.close(); + _multipartController = null; + _tryPropagateControllerState(); + _boundaryIndex = 0; + _state = _boundaryEndingCode; + } + } else { + // Restart matching of the boundary. + _index = _index - _boundaryIndex; + contentStartIndex ??= _index; + _boundaryIndex = 0; + } + + case _lastBoundaryDash2Code: + _expectByteValue(byte, char_code.dash); + _state = _lastBoundaryEndingCode; + + case _lastBoundaryEndingCode: + if (byte == char_code.cr) { + _state = _lastBoundaryEndCode; + } else { + _expectWhitespace(byte); + } + + case _lastBoundaryEndCode: + _expectByteValue(byte, char_code.lf); + _multipartController?.close(); + if (_multipartController != null) { + _multipartController = null; + _tryPropagateControllerState(); + } + _state = _doneCode; + + default: + // Should be unreachable. + assert(false); + break; + } + + // Move to the next byte. + _index++; + } + + // Report any known content. + if (_state == _contentCode && contentStartIndex != null) { + reportData(); + } + + // Resume if at end. + if (_index == _buffer.length) { + _buffer = _placeholderBuffer; + _index = 0; + _subscription.resume(); + } + } +} + +// Used as a placeholder instead of having a nullable buffer. +const _placeholderBuffer = []; diff --git a/pkgs/mime/lib/src/char_code.dart b/pkgs/mime/lib/src/char_code.dart new file mode 100644 index 00000000..4cca1b19 --- /dev/null +++ b/pkgs/mime/lib/src/char_code.dart @@ -0,0 +1,13 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const int ht = 9; +const int lf = 10; +const int cr = 13; +const int sp = 32; +const int dash = 45; +const int colon = 58; +const int upperA = 65; +const int upperZ = 90; +const int lowerA = 97; diff --git a/pkgs/mime/lib/src/default_extension_map.dart b/pkgs/mime/lib/src/default_extension_map.dart new file mode 100644 index 00000000..287c957d --- /dev/null +++ b/pkgs/mime/lib/src/default_extension_map.dart @@ -0,0 +1,1012 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +const Map defaultExtensionMap = { + '123': 'application/vnd.lotus-1-2-3', + '3dml': 'text/vnd.in3d.3dml', + '3ds': 'image/x-3ds', + '3g2': 'video/3gpp2', + '3gp': 'video/3gpp', + '7z': 'application/x-7z-compressed', + 'aab': 'application/x-authorware-bin', + 'aac': 'audio/aac', + 'aam': 'application/x-authorware-map', + 'aas': 'application/x-authorware-seg', + 'abw': 'application/x-abiword', + 'ac': 'application/pkix-attr-cert', + 'acc': 'application/vnd.americandynamics.acc', + 'ace': 'application/x-ace-compressed', + 'acu': 'application/vnd.acucobol', + 'acutc': 'application/vnd.acucorp', + 'adp': 'audio/adpcm', + 'aep': 'application/vnd.audiograph', + 'afm': 'application/x-font-type1', + 'afp': 'application/vnd.ibm.modcap', + 'ahead': 'application/vnd.ahead.space', + 'ai': 'application/postscript', + 'aif': 'audio/x-aiff', + 'aifc': 'audio/x-aiff', + 'aiff': 'audio/x-aiff', + 'air': 'application/vnd.adobe.air-application-installer-package+zip', + 'ait': 'application/vnd.dvb.ait', + 'ami': 'application/vnd.amiga.ami', + 'apk': 'application/vnd.android.package-archive', + 'appcache': 'text/cache-manifest', + 'application': 'application/x-ms-application', + 'apr': 'application/vnd.lotus-approach', + 'arc': 'application/x-freearc', + 'asc': 'application/pgp-signature', + 'asf': 'video/x-ms-asf', + 'asm': 'text/x-asm', + 'aso': 'application/vnd.accpac.simply.aso', + 'asx': 'video/x-ms-asf', + 'atc': 'application/vnd.acucorp', + 'atom': 'application/atom+xml', + 'atomcat': 'application/atomcat+xml', + 'atomsvc': 'application/atomsvc+xml', + 'atx': 'application/vnd.antix.game-component', + 'au': 'audio/basic', + 'avi': 'video/x-msvideo', + 'avif': 'image/avif', + 'aw': 'application/applixware', + 'azf': 'application/vnd.airzip.filesecure.azf', + 'azs': 'application/vnd.airzip.filesecure.azs', + 'azw': 'application/vnd.amazon.ebook', + 'bat': 'application/x-msdownload', + 'bcpio': 'application/x-bcpio', + 'bdf': 'application/x-font-bdf', + 'bdm': 'application/vnd.syncml.dm+wbxml', + 'bed': 'application/vnd.realvnc.bed', + 'bh2': 'application/vnd.fujitsu.oasysprs', + 'bin': 'application/octet-stream', + 'blb': 'application/x-blorb', + 'blorb': 'application/x-blorb', + 'bmi': 'application/vnd.bmi', + 'bmp': 'image/bmp', + 'book': 'application/vnd.framemaker', + 'box': 'application/vnd.previewsystems.box', + 'boz': 'application/x-bzip2', + 'bpk': 'application/octet-stream', + 'btif': 'image/prs.btif', + 'bz': 'application/x-bzip', + 'bz2': 'application/x-bzip2', + 'c': 'text/x-c', + 'c11amc': 'application/vnd.cluetrust.cartomobile-config', + 'c11amz': 'application/vnd.cluetrust.cartomobile-config-pkg', + 'c4d': 'application/vnd.clonk.c4group', + 'c4f': 'application/vnd.clonk.c4group', + 'c4g': 'application/vnd.clonk.c4group', + 'c4p': 'application/vnd.clonk.c4group', + 'c4u': 'application/vnd.clonk.c4group', + 'cab': 'application/vnd.ms-cab-compressed', + 'caf': 'audio/x-caf', + 'cap': 'application/vnd.tcpdump.pcap', + 'car': 'application/vnd.curl.car', + 'cat': 'application/vnd.ms-pki.seccat', + 'cb7': 'application/x-cbr', + 'cba': 'application/x-cbr', + 'cbr': 'application/x-cbr', + 'cbt': 'application/x-cbr', + 'cbz': 'application/x-cbr', + 'cc': 'text/x-c', + 'cct': 'application/x-director', + 'ccxml': 'application/ccxml+xml', + 'cdbcmsg': 'application/vnd.contact.cmsg', + 'cdf': 'application/x-netcdf', + 'cdkey': 'application/vnd.mediastation.cdkey', + 'cdmia': 'application/cdmi-capability', + 'cdmic': 'application/cdmi-container', + 'cdmid': 'application/cdmi-domain', + 'cdmio': 'application/cdmi-object', + 'cdmiq': 'application/cdmi-queue', + 'cdx': 'chemical/x-cdx', + 'cdxml': 'application/vnd.chemdraw+xml', + 'cdy': 'application/vnd.cinderella', + 'cer': 'application/pkix-cert', + 'cfs': 'application/x-cfs-compressed', + 'cgm': 'image/cgm', + 'chat': 'application/x-chat', + 'chm': 'application/vnd.ms-htmlhelp', + 'chrt': 'application/vnd.kde.kchart', + 'cif': 'chemical/x-cif', + 'cii': 'application/vnd.anser-web-certificate-issue-initiation', + 'cil': 'application/vnd.ms-artgalry', + 'cla': 'application/vnd.claymore', + 'class': 'application/java-vm', + 'clkk': 'application/vnd.crick.clicker.keyboard', + 'clkp': 'application/vnd.crick.clicker.palette', + 'clkt': 'application/vnd.crick.clicker.template', + 'clkw': 'application/vnd.crick.clicker.wordbank', + 'clkx': 'application/vnd.crick.clicker', + 'clp': 'application/x-msclip', + 'cmc': 'application/vnd.cosmocaller', + 'cmdf': 'chemical/x-cmdf', + 'cml': 'chemical/x-cml', + 'cmp': 'application/vnd.yellowriver-custom-menu', + 'cmx': 'image/x-cmx', + 'cod': 'application/vnd.rim.cod', + 'com': 'application/x-msdownload', + 'conf': 'text/plain', + 'cpio': 'application/x-cpio', + 'cpp': 'text/x-c', + 'cpt': 'application/mac-compactpro', + 'crd': 'application/x-mscardfile', + 'crl': 'application/pkix-crl', + 'crt': 'application/x-x509-ca-cert', + 'cryptonote': 'application/vnd.rig.cryptonote', + 'csh': 'application/x-csh', + 'csml': 'chemical/x-csml', + 'csp': 'application/vnd.commonspace', + 'css': 'text/css', + 'cst': 'application/x-director', + 'csv': 'text/csv', + 'cu': 'application/cu-seeme', + 'curl': 'text/vnd.curl', + 'cww': 'application/prs.cww', + 'cxt': 'application/x-director', + 'cxx': 'text/x-c', + 'dae': 'model/vnd.collada+xml', + 'daf': 'application/vnd.mobius.daf', + 'dart': 'text/x-dart', + 'dataless': 'application/vnd.fdsn.seed', + 'davmount': 'application/davmount+xml', + 'dbk': 'application/docbook+xml', + 'dcm': 'application/dicom', + 'dcr': 'application/x-director', + 'dcurl': 'text/vnd.curl.dcurl', + 'dd2': 'application/vnd.oma.dd2+xml', + 'ddd': 'application/vnd.fujixerox.ddd', + 'deb': 'application/x-debian-package', + 'def': 'text/plain', + 'deploy': 'application/octet-stream', + 'der': 'application/x-x509-ca-cert', + 'dfac': 'application/vnd.dreamfactory', + 'dgc': 'application/x-dgc-compressed', + 'dic': 'text/x-c', + 'dir': 'application/x-director', + 'dis': 'application/vnd.mobius.dis', + 'dist': 'application/octet-stream', + 'distz': 'application/octet-stream', + 'djv': 'image/vnd.djvu', + 'djvu': 'image/vnd.djvu', + 'dll': 'application/x-msdownload', + 'dmg': 'application/x-apple-diskimage', + 'dmp': 'application/vnd.tcpdump.pcap', + 'dms': 'application/octet-stream', + 'dna': 'application/vnd.dna', + 'doc': 'application/msword', + 'docm': 'application/vnd.ms-word.document.macroenabled.12', + 'docx': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot': 'application/msword', + 'dotm': 'application/vnd.ms-word.template.macroenabled.12', + 'dotx': + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp': 'application/vnd.osgi.dp', + 'dpg': 'application/vnd.dpgraph', + 'dra': 'audio/vnd.dra', + 'dsc': 'text/prs.lines.tag', + 'dssc': 'application/dssc+der', + 'dtb': 'application/x-dtbook+xml', + 'dtd': 'application/xml-dtd', + 'dts': 'audio/vnd.dts', + 'dtshd': 'audio/vnd.dts.hd', + 'dump': 'application/octet-stream', + 'dvb': 'video/vnd.dvb.file', + 'dvi': 'application/x-dvi', + 'dwf': 'model/vnd.dwf', + 'dwg': 'image/vnd.dwg', + 'dxf': 'image/vnd.dxf', + 'dxp': 'application/vnd.spotfire.dxp', + 'dxr': 'application/x-director', + 'ecelp4800': 'audio/vnd.nuera.ecelp4800', + 'ecelp7470': 'audio/vnd.nuera.ecelp7470', + 'ecelp9600': 'audio/vnd.nuera.ecelp9600', + 'ecma': 'application/ecmascript', + 'edm': 'application/vnd.novadigm.edm', + 'edx': 'application/vnd.novadigm.edx', + 'efif': 'application/vnd.picsel', + 'ei6': 'application/vnd.pg.osasli', + 'elc': 'application/octet-stream', + 'emf': 'application/x-msmetafile', + 'eml': 'message/rfc822', + 'emma': 'application/emma+xml', + 'emz': 'application/x-msmetafile', + 'eol': 'audio/vnd.digital-winds', + 'eot': 'application/vnd.ms-fontobject', + 'eps': 'application/postscript', + 'epub': 'application/epub+zip', + 'es3': 'application/vnd.eszigno3+xml', + 'esa': 'application/vnd.osgi.subsystem', + 'esf': 'application/vnd.epson.esf', + 'et3': 'application/vnd.eszigno3+xml', + 'etx': 'text/x-setext', + 'eva': 'application/x-eva', + 'evy': 'application/x-envoy', + 'exe': 'application/x-msdownload', + 'exi': 'application/exi', + 'ext': 'application/vnd.novadigm.ext', + 'ez': 'application/andrew-inset', + 'ez2': 'application/vnd.ezpix-album', + 'ez3': 'application/vnd.ezpix-package', + 'f': 'text/x-fortran', + 'f4v': 'video/x-f4v', + 'f77': 'text/x-fortran', + 'f90': 'text/x-fortran', + 'fbs': 'image/vnd.fastbidsheet', + 'fcdt': 'application/vnd.adobe.formscentral.fcdt', + 'fcs': 'application/vnd.isac.fcs', + 'fdf': 'application/vnd.fdf', + 'fe_launch': 'application/vnd.denovo.fcselayout-link', + 'fg5': 'application/vnd.fujitsu.oasysgp', + 'fgd': 'application/x-director', + 'fh': 'image/x-freehand', + 'fh4': 'image/x-freehand', + 'fh5': 'image/x-freehand', + 'fh7': 'image/x-freehand', + 'fhc': 'image/x-freehand', + 'fig': 'application/x-xfig', + 'flac': 'audio/x-flac', + 'fli': 'video/x-fli', + 'flo': 'application/vnd.micrografx.flo', + 'flv': 'video/x-flv', + 'flw': 'application/vnd.kde.kivio', + 'flx': 'text/vnd.fmi.flexstor', + 'fly': 'text/vnd.fly', + 'fm': 'application/vnd.framemaker', + 'fnc': 'application/vnd.frogans.fnc', + 'for': 'text/x-fortran', + 'fpx': 'image/vnd.fpx', + 'frame': 'application/vnd.framemaker', + 'fsc': 'application/vnd.fsc.weblaunch', + 'fst': 'image/vnd.fst', + 'ftc': 'application/vnd.fluxtime.clip', + 'fti': 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt': 'video/vnd.fvt', + 'fxp': 'application/vnd.adobe.fxp', + 'fxpl': 'application/vnd.adobe.fxp', + 'fzs': 'application/vnd.fuzzysheet', + 'g2w': 'application/vnd.geoplan', + 'g3': 'image/g3fax', + 'g3w': 'application/vnd.geospace', + 'gac': 'application/vnd.groove-account', + 'gam': 'application/x-tads', + 'gbr': 'application/rpki-ghostbusters', + 'gca': 'application/x-gca-compressed', + 'gdl': 'model/vnd.gdl', + 'geo': 'application/vnd.dynageo', + 'gex': 'application/vnd.geometry-explorer', + 'ggb': 'application/vnd.geogebra.file', + 'ggt': 'application/vnd.geogebra.tool', + 'ghf': 'application/vnd.groove-help', + 'gif': 'image/gif', + 'gim': 'application/vnd.groove-identity-message', + 'glb': 'model/gltf-binary', + 'gltf': 'model/gltf+json', + 'gml': 'application/gml+xml', + 'gmx': 'application/vnd.gmx', + 'gnumeric': 'application/x-gnumeric', + 'gph': 'application/vnd.flographit', + 'gpx': 'application/gpx+xml', + 'gqf': 'application/vnd.grafeq', + 'gqs': 'application/vnd.grafeq', + 'gram': 'application/srgs', + 'gramps': 'application/x-gramps-xml', + 'gre': 'application/vnd.geometry-explorer', + 'grv': 'application/vnd.groove-injector', + 'grxml': 'application/srgs+xml', + 'gsf': 'application/x-font-ghostscript', + 'gtar': 'application/x-gtar', + 'gtm': 'application/vnd.groove-tool-message', + 'gtw': 'model/vnd.gtw', + 'gv': 'text/vnd.graphviz', + 'gxf': 'application/gxf', + 'gxt': 'application/vnd.geonext', + 'h': 'text/x-c', + 'h261': 'video/h261', + 'h263': 'video/h263', + 'h264': 'video/h264', + 'hal': 'application/vnd.hal+xml', + 'hbci': 'application/vnd.hbci', + 'hdf': 'application/x-hdf', + 'heic': 'image/heic', + 'heif': 'image/heif', + 'hh': 'text/x-c', + 'hlp': 'application/winhlp', + 'hpgl': 'application/vnd.hp-hpgl', + 'hpid': 'application/vnd.hp-hpid', + 'hps': 'application/vnd.hp-hps', + 'hqx': 'application/mac-binhex40', + 'htke': 'application/vnd.kenameaapp', + 'htm': 'text/html', + 'html': 'text/html', + 'hvd': 'application/vnd.yamaha.hv-dic', + 'hvp': 'application/vnd.yamaha.hv-voice', + 'hvs': 'application/vnd.yamaha.hv-script', + 'i2g': 'application/vnd.intergeo', + 'icc': 'application/vnd.iccprofile', + 'ice': 'x-conference/x-cooltalk', + 'icm': 'application/vnd.iccprofile', + 'ico': 'image/x-icon', + 'ics': 'text/calendar', + 'ief': 'image/ief', + 'ifb': 'text/calendar', + 'ifm': 'application/vnd.shana.informed.formdata', + 'iges': 'model/iges', + 'igl': 'application/vnd.igloader', + 'igm': 'application/vnd.insors.igm', + 'igs': 'model/iges', + 'igx': 'application/vnd.micrografx.igx', + 'iif': 'application/vnd.shana.informed.interchange', + 'imp': 'application/vnd.accpac.simply.imp', + 'ims': 'application/vnd.ms-ims', + 'in': 'text/plain', + 'ink': 'application/inkml+xml', + 'inkml': 'application/inkml+xml', + 'install': 'application/x-install-instructions', + 'iota': 'application/vnd.astraea-software.iota', + 'ipfix': 'application/ipfix', + 'ipk': 'application/vnd.shana.informed.package', + 'irm': 'application/vnd.ibm.rights-management', + 'irp': 'application/vnd.irepository.package+xml', + 'iso': 'application/x-iso9660-image', + 'itp': 'application/vnd.shana.informed.formtemplate', + 'ivp': 'application/vnd.immervision-ivp', + 'ivu': 'application/vnd.immervision-ivu', + 'jad': 'text/vnd.sun.j2me.app-descriptor', + 'jam': 'application/vnd.jam', + 'jar': 'application/java-archive', + 'java': 'text/x-java-source', + 'jisp': 'application/vnd.jisp', + 'jlt': 'application/vnd.hp-jlyt', + 'jnlp': 'application/x-java-jnlp-file', + 'joda': 'application/vnd.joost.joda-archive', + 'jpe': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'jpgm': 'video/jpm', + 'jpgv': 'video/jpeg', + 'jpm': 'video/jpm', + 'js': 'text/javascript', + 'json': 'application/json', + 'jsonml': 'application/jsonml+json', + 'kar': 'audio/midi', + 'karbon': 'application/vnd.kde.karbon', + 'kfo': 'application/vnd.kde.kformula', + 'kia': 'application/vnd.kidspiration', + 'kml': 'application/vnd.google-earth.kml+xml', + 'kmz': 'application/vnd.google-earth.kmz', + 'kne': 'application/vnd.kinar', + 'knp': 'application/vnd.kinar', + 'kon': 'application/vnd.kde.kontour', + 'kpr': 'application/vnd.kde.kpresenter', + 'kpt': 'application/vnd.kde.kpresenter', + 'kpxx': 'application/vnd.ds-keypoint', + 'ksp': 'application/vnd.kde.kspread', + 'ktr': 'application/vnd.kahootz', + 'ktx': 'image/ktx', + 'ktz': 'application/vnd.kahootz', + 'kwd': 'application/vnd.kde.kword', + 'kwt': 'application/vnd.kde.kword', + 'lasxml': 'application/vnd.las.las+xml', + 'latex': 'application/x-latex', + 'lbd': 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe': 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les': 'application/vnd.hhe.lesson-player', + 'lha': 'application/x-lzh-compressed', + 'link66': 'application/vnd.route66.link66+xml', + 'list': 'text/plain', + 'list3820': 'application/vnd.ibm.modcap', + 'listafp': 'application/vnd.ibm.modcap', + 'lnk': 'application/x-ms-shortcut', + 'log': 'text/plain', + 'lostxml': 'application/lost+xml', + 'lrf': 'application/octet-stream', + 'lrm': 'application/vnd.ms-lrm', + 'ltf': 'application/vnd.frogans.ltf', + 'lvp': 'audio/vnd.lucent.voice', + 'lwp': 'application/vnd.lotus-wordpro', + 'lzh': 'application/x-lzh-compressed', + 'm13': 'application/x-msmediaview', + 'm14': 'application/x-msmediaview', + 'm1v': 'video/mpeg', + 'm21': 'application/mp21', + 'm2a': 'audio/mpeg', + 'm2v': 'video/mpeg', + 'm3a': 'audio/mpeg', + 'm3u': 'audio/x-mpegurl', + 'm3u8': 'application/vnd.apple.mpegurl', + // Source: https://datatracker.ietf.org/doc/html/rfc4337#section-2 + 'm4a': 'audio/mp4', + 'm4b': 'audio/mp4', + 'm4u': 'video/vnd.mpegurl', + 'm4v': 'video/x-m4v', + 'ma': 'application/mathematica', + 'mads': 'application/mads+xml', + 'mag': 'application/vnd.ecowin.chart', + 'maker': 'application/vnd.framemaker', + 'man': 'text/troff', + 'mar': 'application/octet-stream', + 'mathml': 'application/mathml+xml', + 'mb': 'application/mathematica', + 'mbk': 'application/vnd.mobius.mbk', + 'mbox': 'application/mbox', + 'mc1': 'application/vnd.medcalcdata', + 'mcd': 'application/vnd.mcd', + 'mcurl': 'text/vnd.curl.mcurl', + // https://www.rfc-editor.org/rfc/rfc7763 + 'md': 'text/markdown', + 'markdown': 'text/markdown', + 'mdb': 'application/x-msaccess', + 'mdi': 'image/vnd.ms-modi', + 'me': 'text/troff', + 'mesh': 'model/mesh', + 'meta4': 'application/metalink4+xml', + 'metalink': 'application/metalink+xml', + 'mets': 'application/mets+xml', + 'mfm': 'application/vnd.mfmp', + 'mft': 'application/rpki-manifest', + 'mgp': 'application/vnd.osgeo.mapguide.package', + 'mgz': 'application/vnd.proteus.magazine', + 'mid': 'audio/midi', + 'midi': 'audio/midi', + 'mie': 'application/x-mie', + 'mif': 'application/vnd.mif', + 'mime': 'message/rfc822', + 'mj2': 'video/mj2', + 'mjp2': 'video/mj2', + 'mjs': 'text/javascript', + 'mk3d': 'video/x-matroska', + 'mka': 'audio/x-matroska', + 'mks': 'video/x-matroska', + 'mkv': 'video/x-matroska', + 'mlp': 'application/vnd.dolby.mlp', + 'mmd': 'application/vnd.chipnuts.karaoke-mmd', + 'mmf': 'application/vnd.smaf', + 'mmr': 'image/vnd.fujixerox.edmics-mmr', + 'mng': 'video/x-mng', + 'mny': 'application/x-msmoney', + 'mobi': 'application/x-mobipocket-ebook', + 'mods': 'application/mods+xml', + 'mov': 'video/quicktime', + 'movie': 'video/x-sgi-movie', + 'mp2': 'audio/mpeg', + 'mp21': 'application/mp21', + 'mp2a': 'audio/mpeg', + 'mp3': 'audio/mpeg', + 'mp4': 'video/mp4', + 'mp4a': 'audio/mp4', + 'mp4s': 'application/mp4', + 'mp4v': 'video/mp4', + 'mpc': 'application/vnd.mophun.certificate', + 'mpe': 'video/mpeg', + 'mpeg': 'video/mpeg', + 'mpg': 'video/mpeg', + 'mpg4': 'video/mp4', + 'mpga': 'audio/mpeg', + 'mpkg': 'application/vnd.apple.installer+xml', + 'mpm': 'application/vnd.blueice.multipass', + 'mpn': 'application/vnd.mophun.application', + 'mpp': 'application/vnd.ms-project', + 'mpt': 'application/vnd.ms-project', + 'mpy': 'application/vnd.ibm.minipay', + 'mqy': 'application/vnd.mobius.mqy', + 'mrc': 'application/marc', + 'mrcx': 'application/marcxml+xml', + 'ms': 'text/troff', + 'mscml': 'application/mediaservercontrol+xml', + 'mseed': 'application/vnd.fdsn.mseed', + 'mseq': 'application/vnd.mseq', + 'msf': 'application/vnd.epson.msf', + 'msh': 'model/mesh', + 'msi': 'application/x-msdownload', + 'msl': 'application/vnd.mobius.msl', + 'msty': 'application/vnd.muvee.style', + 'mts': 'model/vnd.mts', + 'mus': 'application/vnd.musician', + 'musicxml': 'application/vnd.recordare.musicxml+xml', + 'mvb': 'application/x-msmediaview', + 'mwf': 'application/vnd.mfer', + 'mxf': 'application/mxf', + 'mxl': 'application/vnd.recordare.musicxml', + 'mxml': 'application/xv+xml', + 'mxs': 'application/vnd.triscape.mxs', + 'mxu': 'video/vnd.mpegurl', + 'n-gage': 'application/vnd.nokia.n-gage.symbian.install', + 'n3': 'text/n3', + 'nb': 'application/mathematica', + 'nbp': 'application/vnd.wolfram.player', + 'nc': 'application/x-netcdf', + 'ncx': 'application/x-dtbncx+xml', + 'nfo': 'text/x-nfo', + 'ngdat': 'application/vnd.nokia.n-gage.data', + 'nitf': 'application/vnd.nitf', + 'nlu': 'application/vnd.neurolanguage.nlu', + 'nml': 'application/vnd.enliven', + 'nnd': 'application/vnd.noblenet-directory', + 'nns': 'application/vnd.noblenet-sealer', + 'nnw': 'application/vnd.noblenet-web', + 'npx': 'image/vnd.net-fpx', + 'nsc': 'application/x-conference', + 'nsf': 'application/vnd.lotus-notes', + 'ntf': 'application/vnd.nitf', + 'nzb': 'application/x-nzb', + 'oa2': 'application/vnd.fujitsu.oasys2', + 'oa3': 'application/vnd.fujitsu.oasys3', + 'oas': 'application/vnd.fujitsu.oasys', + 'obd': 'application/x-msbinder', + 'obj': 'application/x-tgif', + 'oda': 'application/oda', + 'odb': 'application/vnd.oasis.opendocument.database', + 'odc': 'application/vnd.oasis.opendocument.chart', + 'odf': 'application/vnd.oasis.opendocument.formula', + 'odft': 'application/vnd.oasis.opendocument.formula-template', + 'odg': 'application/vnd.oasis.opendocument.graphics', + 'odi': 'application/vnd.oasis.opendocument.image', + 'odm': 'application/vnd.oasis.opendocument.text-master', + 'odp': 'application/vnd.oasis.opendocument.presentation', + 'ods': 'application/vnd.oasis.opendocument.spreadsheet', + 'odt': 'application/vnd.oasis.opendocument.text', + 'oga': 'audio/ogg', + 'ogg': 'audio/ogg', + 'ogv': 'video/ogg', + 'ogx': 'application/ogg', + 'omdoc': 'application/omdoc+xml', + 'onepkg': 'application/onenote', + 'onetmp': 'application/onenote', + 'onetoc': 'application/onenote', + 'onetoc2': 'application/onenote', + 'opf': 'application/oebps-package+xml', + 'opml': 'text/x-opml', + 'oprc': 'application/vnd.palm', + 'org': 'application/vnd.lotus-organizer', + 'osf': 'application/vnd.yamaha.openscoreformat', + 'osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'otc': 'application/vnd.oasis.opendocument.chart-template', + 'otf': 'application/x-font-otf', + 'otg': 'application/vnd.oasis.opendocument.graphics-template', + 'oth': 'application/vnd.oasis.opendocument.text-web', + 'oti': 'application/vnd.oasis.opendocument.image-template', + 'otp': 'application/vnd.oasis.opendocument.presentation-template', + 'ots': 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott': 'application/vnd.oasis.opendocument.text-template', + 'oxps': 'application/oxps', + 'oxt': 'application/vnd.openofficeorg.extension', + 'p': 'text/x-pascal', + 'p10': 'application/pkcs10', + 'p12': 'application/x-pkcs12', + 'p7b': 'application/x-pkcs7-certificates', + 'p7c': 'application/pkcs7-mime', + 'p7m': 'application/pkcs7-mime', + 'p7r': 'application/x-pkcs7-certreqresp', + 'p7s': 'application/pkcs7-signature', + 'p8': 'application/pkcs8', + 'pas': 'text/x-pascal', + 'paw': 'application/vnd.pawaafile', + 'pbd': 'application/vnd.powerbuilder6', + 'pbm': 'image/x-portable-bitmap', + 'pcap': 'application/vnd.tcpdump.pcap', + 'pcf': 'application/x-font-pcf', + 'pcl': 'application/vnd.hp-pcl', + 'pclxl': 'application/vnd.hp-pclxl', + 'pct': 'image/x-pict', + 'pcurl': 'application/vnd.curl.pcurl', + 'pcx': 'image/x-pcx', + 'pdb': 'application/vnd.palm', + 'pdf': 'application/pdf', + 'pfa': 'application/x-font-type1', + 'pfb': 'application/x-font-type1', + 'pfm': 'application/x-font-type1', + 'pfr': 'application/font-tdpfr', + 'pfx': 'application/x-pkcs12', + 'pgm': 'image/x-portable-graymap', + 'pgn': 'application/x-chess-pgn', + 'pgp': 'application/pgp-encrypted', + 'pic': 'image/x-pict', + 'pkg': 'application/octet-stream', + 'pki': 'application/pkixcmp', + 'pkipath': 'application/pkix-pkipath', + 'plb': 'application/vnd.3gpp.pic-bw-large', + 'plc': 'application/vnd.mobius.plc', + 'plf': 'application/vnd.pocketlearn', + 'pls': 'application/pls+xml', + 'pml': 'application/vnd.ctc-posml', + 'png': 'image/png', + 'pnm': 'image/x-portable-anymap', + 'portpkg': 'application/vnd.macports.portpkg', + 'pot': 'application/vnd.ms-powerpoint', + 'potm': 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'potx': + 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppam': 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'ppd': 'application/vnd.cups-ppd', + 'ppm': 'image/x-portable-pixmap', + 'pps': 'application/vnd.ms-powerpoint', + 'ppsm': 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'ppsx': + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt': 'application/vnd.ms-powerpoint', + 'pptm': 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'pptx': + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa': 'application/vnd.palm', + 'prc': 'application/x-mobipocket-ebook', + 'pre': 'application/vnd.lotus-freelance', + 'prf': 'application/pics-rules', + 'ps': 'application/postscript', + 'psb': 'application/vnd.3gpp.pic-bw-small', + 'psd': 'image/vnd.adobe.photoshop', + 'psf': 'application/x-font-linux-psf', + 'pskcxml': 'application/pskc+xml', + 'ptid': 'application/vnd.pvi.ptid1', + 'pub': 'application/x-mspublisher', + 'pvb': 'application/vnd.3gpp.pic-bw-var', + 'pwn': 'application/vnd.3m.post-it-notes', + 'pya': 'audio/vnd.ms-playready.media.pya', + 'pyv': 'video/vnd.ms-playready.media.pyv', + 'qam': 'application/vnd.epson.quickanime', + 'qbo': 'application/vnd.intu.qbo', + 'qfx': 'application/vnd.intu.qfx', + 'qps': 'application/vnd.publishare-delta-tree', + 'qt': 'video/quicktime', + 'qwd': 'application/vnd.quark.quarkxpress', + 'qwt': 'application/vnd.quark.quarkxpress', + 'qxb': 'application/vnd.quark.quarkxpress', + 'qxd': 'application/vnd.quark.quarkxpress', + 'qxl': 'application/vnd.quark.quarkxpress', + 'qxt': 'application/vnd.quark.quarkxpress', + 'ra': 'audio/x-pn-realaudio', + 'ram': 'audio/x-pn-realaudio', + 'rar': 'application/x-rar-compressed', + 'ras': 'image/x-cmu-raster', + 'rcprofile': 'application/vnd.ipunplugged.rcprofile', + 'rdf': 'application/rdf+xml', + 'rdz': 'application/vnd.data-vision.rdz', + 'rep': 'application/vnd.businessobjects', + 'res': 'application/x-dtbresource+xml', + 'rgb': 'image/x-rgb', + 'rif': 'application/reginfo+xml', + 'rip': 'audio/vnd.rip', + 'ris': 'application/x-research-info-systems', + 'rl': 'application/resource-lists+xml', + 'rlc': 'image/vnd.fujixerox.edmics-rlc', + 'rld': 'application/resource-lists-diff+xml', + 'rm': 'application/vnd.rn-realmedia', + 'rmi': 'audio/midi', + 'rmp': 'audio/x-pn-realaudio-plugin', + 'rms': 'application/vnd.jcp.javame.midlet-rms', + 'rmvb': 'application/vnd.rn-realmedia-vbr', + 'rnc': 'application/relax-ng-compact-syntax', + 'roa': 'application/rpki-roa', + 'roff': 'text/troff', + 'rp9': 'application/vnd.cloanto.rp9', + 'rpss': 'application/vnd.nokia.radio-presets', + 'rpst': 'application/vnd.nokia.radio-preset', + 'rq': 'application/sparql-query', + 'rs': 'application/rls-services+xml', + 'rsd': 'application/rsd+xml', + 'rss': 'application/rss+xml', + 'rtf': 'application/rtf', + 'rtx': 'text/richtext', + 's': 'text/x-asm', + 's3m': 'audio/s3m', + 'saf': 'application/vnd.yamaha.smaf-audio', + 'sbml': 'application/sbml+xml', + 'sc': 'application/vnd.ibm.secure-container', + 'scd': 'application/x-msschedule', + 'scm': 'application/vnd.lotus-screencam', + 'scq': 'application/scvp-cv-request', + 'scs': 'application/scvp-cv-response', + 'scurl': 'text/vnd.curl.scurl', + 'sda': 'application/vnd.stardivision.draw', + 'sdc': 'application/vnd.stardivision.calc', + 'sdd': 'application/vnd.stardivision.impress', + 'sdkd': 'application/vnd.solent.sdkm+xml', + 'sdkm': 'application/vnd.solent.sdkm+xml', + 'sdp': 'application/sdp', + 'sdw': 'application/vnd.stardivision.writer', + 'see': 'application/vnd.seemail', + 'seed': 'application/vnd.fdsn.seed', + 'sema': 'application/vnd.sema', + 'semd': 'application/vnd.semd', + 'semf': 'application/vnd.semf', + 'ser': 'application/java-serialized-object', + 'setpay': 'application/set-payment-initiation', + 'setreg': 'application/set-registration-initiation', + 'sfd-hdstx': 'application/vnd.hydrostatix.sof-data', + 'sfs': 'application/vnd.spotfire.sfs', + 'sfv': 'text/x-sfv', + 'sgi': 'image/sgi', + 'sgl': 'application/vnd.stardivision.writer-global', + 'sgm': 'text/sgml', + 'sgml': 'text/sgml', + 'sh': 'application/x-sh', + 'shar': 'application/x-shar', + 'shf': 'application/shf+xml', + 'sid': 'image/x-mrsid-image', + 'sig': 'application/pgp-signature', + 'sil': 'audio/silk', + 'silo': 'model/mesh', + 'sis': 'application/vnd.symbian.install', + 'sisx': 'application/vnd.symbian.install', + 'sit': 'application/x-stuffit', + 'sitx': 'application/x-stuffitx', + 'skd': 'application/vnd.koan', + 'skm': 'application/vnd.koan', + 'skp': 'application/vnd.koan', + 'skt': 'application/vnd.koan', + 'sldm': 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx': 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slt': 'application/vnd.epson.salt', + 'sm': 'application/vnd.stepmania.stepchart', + 'smf': 'application/vnd.stardivision.math', + 'smi': 'application/smil+xml', + 'smil': 'application/smil+xml', + 'smv': 'video/x-smv', + 'smzip': 'application/vnd.stepmania.package', + 'snd': 'audio/basic', + 'snf': 'application/x-font-snf', + 'so': 'application/octet-stream', + 'spc': 'application/x-pkcs7-certificates', + 'spf': 'application/vnd.yamaha.smaf-phrase', + 'spl': 'application/x-futuresplash', + 'spot': 'text/vnd.in3d.spot', + 'spp': 'application/scvp-vp-response', + 'spq': 'application/scvp-vp-request', + 'spx': 'audio/ogg', + 'sql': 'application/x-sql', + 'src': 'application/x-wais-source', + 'srt': 'application/x-subrip', + 'sru': 'application/sru+xml', + 'srx': 'application/sparql-results+xml', + 'ssdl': 'application/ssdl+xml', + 'sse': 'application/vnd.kodak-descriptor', + 'ssf': 'application/vnd.epson.ssf', + 'ssml': 'application/ssml+xml', + 'st': 'application/vnd.sailingtracker.track', + 'stc': 'application/vnd.sun.xml.calc.template', + 'std': 'application/vnd.sun.xml.draw.template', + 'stf': 'application/vnd.wt.stf', + 'sti': 'application/vnd.sun.xml.impress.template', + 'stk': 'application/hyperstudio', + 'stl': 'application/vnd.ms-pki.stl', + 'str': 'application/vnd.pg.format', + 'stw': 'application/vnd.sun.xml.writer.template', + 'sub': 'text/vnd.dvb.subtitle', + 'sus': 'application/vnd.sus-calendar', + 'susp': 'application/vnd.sus-calendar', + 'sv4cpio': 'application/x-sv4cpio', + 'sv4crc': 'application/x-sv4crc', + 'svc': 'application/vnd.dvb.service', + 'svd': 'application/vnd.svd', + 'svg': 'image/svg+xml', + 'svgz': 'image/svg+xml', + 'swa': 'application/x-director', + 'swf': 'application/x-shockwave-flash', + 'swi': 'application/vnd.aristanetworks.swi', + 'sxc': 'application/vnd.sun.xml.calc', + 'sxd': 'application/vnd.sun.xml.draw', + 'sxg': 'application/vnd.sun.xml.writer.global', + 'sxi': 'application/vnd.sun.xml.impress', + 'sxm': 'application/vnd.sun.xml.math', + 'sxw': 'application/vnd.sun.xml.writer', + 't': 'text/troff', + 't3': 'application/x-t3vm-image', + 'taglet': 'application/vnd.mynfc', + 'tao': 'application/vnd.tao.intent-module-archive', + 'tar': 'application/x-tar', + 'tcap': 'application/vnd.3gpp2.tcap', + 'tcl': 'application/x-tcl', + 'teacher': 'application/vnd.smart.teacher', + 'tei': 'application/tei+xml', + 'teicorpus': 'application/tei+xml', + 'tex': 'application/x-tex', + 'texi': 'application/x-texinfo', + 'texinfo': 'application/x-texinfo', + 'text': 'text/plain', + 'tfi': 'application/thraud+xml', + 'tfm': 'application/x-tex-tfm', + 'tga': 'image/x-tga', + 'thmx': 'application/vnd.ms-officetheme', + 'tif': 'image/tiff', + 'tiff': 'image/tiff', + 'tmo': 'application/vnd.tmobile-livetv', + // Source: https://toml.io/en/v1.0.0#mime-type + 'toml': 'application/toml', + 'torrent': 'application/x-bittorrent', + 'tpl': 'application/vnd.groove-tool-template', + 'tpt': 'application/vnd.trid.tpt', + 'tr': 'text/troff', + 'tra': 'application/vnd.trueapp', + 'trm': 'application/x-msterminal', + 'tsd': 'application/timestamped-data', + 'tsv': 'text/tab-separated-values', + 'ttc': 'application/x-font-ttf', + 'ttf': 'application/x-font-ttf', + 'ttl': 'text/turtle', + 'twd': 'application/vnd.simtech-mindmapper', + 'twds': 'application/vnd.simtech-mindmapper', + 'txd': 'application/vnd.genomatix.tuxedo', + 'txf': 'application/vnd.mobius.txf', + 'txt': 'text/plain', + 'u32': 'application/x-authorware-bin', + 'udeb': 'application/x-debian-package', + 'ufd': 'application/vnd.ufdl', + 'ufdl': 'application/vnd.ufdl', + 'ulx': 'application/x-glulx', + 'umj': 'application/vnd.umajin', + 'unityweb': 'application/vnd.unity', + 'uoml': 'application/vnd.uoml+xml', + 'uri': 'text/uri-list', + 'uris': 'text/uri-list', + 'urls': 'text/uri-list', + 'ustar': 'application/x-ustar', + 'utz': 'application/vnd.uiq.theme', + 'uu': 'text/x-uuencode', + 'uva': 'audio/vnd.dece.audio', + 'uvd': 'application/vnd.dece.data', + 'uvf': 'application/vnd.dece.data', + 'uvg': 'image/vnd.dece.graphic', + 'uvh': 'video/vnd.dece.hd', + 'uvi': 'image/vnd.dece.graphic', + 'uvm': 'video/vnd.dece.mobile', + 'uvp': 'video/vnd.dece.pd', + 'uvs': 'video/vnd.dece.sd', + 'uvt': 'application/vnd.dece.ttml+xml', + 'uvu': 'video/vnd.uvvu.mp4', + 'uvv': 'video/vnd.dece.video', + 'uvva': 'audio/vnd.dece.audio', + 'uvvd': 'application/vnd.dece.data', + 'uvvf': 'application/vnd.dece.data', + 'uvvg': 'image/vnd.dece.graphic', + 'uvvh': 'video/vnd.dece.hd', + 'uvvi': 'image/vnd.dece.graphic', + 'uvvm': 'video/vnd.dece.mobile', + 'uvvp': 'video/vnd.dece.pd', + 'uvvs': 'video/vnd.dece.sd', + 'uvvt': 'application/vnd.dece.ttml+xml', + 'uvvu': 'video/vnd.uvvu.mp4', + 'uvvv': 'video/vnd.dece.video', + 'uvvx': 'application/vnd.dece.unspecified', + 'uvvz': 'application/vnd.dece.zip', + 'uvx': 'application/vnd.dece.unspecified', + 'uvz': 'application/vnd.dece.zip', + 'vcard': 'text/vcard', + 'vcd': 'application/x-cdlink', + 'vcf': 'text/x-vcard', + 'vcg': 'application/vnd.groove-vcard', + 'vcs': 'text/x-vcalendar', + 'vcx': 'application/vnd.vcx', + 'vis': 'application/vnd.visionary', + 'viv': 'video/vnd.vivo', + 'vob': 'video/x-ms-vob', + 'vor': 'application/vnd.stardivision.writer', + 'vox': 'application/x-authorware-bin', + 'vrml': 'model/vrml', + 'vsd': 'application/vnd.visio', + 'vsf': 'application/vnd.vsf', + 'vss': 'application/vnd.visio', + 'vst': 'application/vnd.visio', + 'vsw': 'application/vnd.visio', + 'vtu': 'model/vnd.vtu', + 'vxml': 'application/voicexml+xml', + 'w3d': 'application/x-director', + 'wad': 'application/x-doom', + 'wasm': 'application/wasm', + 'wav': 'audio/x-wav', + 'wax': 'audio/x-ms-wax', + 'wbmp': 'image/vnd.wap.wbmp', + 'wbs': 'application/vnd.criticaltools.wbs+xml', + 'wbxml': 'application/vnd.wap.wbxml', + 'wcm': 'application/vnd.ms-works', + 'wdb': 'application/vnd.ms-works', + 'wdp': 'image/vnd.ms-photo', + 'weba': 'audio/webm', + 'webm': 'video/webm', + // Source: https://w3c.github.io/manifest/#media-type-registration + 'webmanifest': 'application/manifest+json', + 'webp': 'image/webp', + 'wg': 'application/vnd.pmi.widget', + 'wgt': 'application/widget', + 'wks': 'application/vnd.ms-works', + 'wm': 'video/x-ms-wm', + 'wma': 'audio/x-ms-wma', + 'wmd': 'application/x-ms-wmd', + 'wmf': 'application/x-msmetafile', + 'wml': 'text/vnd.wap.wml', + 'wmlc': 'application/vnd.wap.wmlc', + 'wmls': 'text/vnd.wap.wmlscript', + 'wmlsc': 'application/vnd.wap.wmlscriptc', + 'wmv': 'video/x-ms-wmv', + 'wmx': 'video/x-ms-wmx', + 'wmz': 'application/x-ms-wmz', + 'woff': 'application/x-font-woff', + 'woff2': 'font/woff2', + 'wpd': 'application/vnd.wordperfect', + 'wpl': 'application/vnd.ms-wpl', + 'wps': 'application/vnd.ms-works', + 'wqd': 'application/vnd.wqd', + 'wri': 'application/x-mswrite', + 'wrl': 'model/vrml', + 'wsdl': 'application/wsdl+xml', + 'wspolicy': 'application/wspolicy+xml', + 'wtb': 'application/vnd.webturbo', + 'wvx': 'video/x-ms-wvx', + 'x32': 'application/x-authorware-bin', + 'x3d': 'model/x3d+xml', + 'x3db': 'model/x3d+binary', + 'x3dbz': 'model/x3d+binary', + 'x3dv': 'model/x3d+vrml', + 'x3dvz': 'model/x3d+vrml', + 'x3dz': 'model/x3d+xml', + 'xaml': 'application/xaml+xml', + 'xap': 'application/x-silverlight-app', + 'xar': 'application/vnd.xara', + 'xbap': 'application/x-ms-xbap', + 'xbd': 'application/vnd.fujixerox.docuworks.binder', + 'xbm': 'image/x-xbitmap', + 'xdf': 'application/xcap-diff+xml', + 'xdm': 'application/vnd.syncml.dm+xml', + 'xdp': 'application/vnd.adobe.xdp+xml', + 'xdssc': 'application/dssc+xml', + 'xdw': 'application/vnd.fujixerox.docuworks', + 'xenc': 'application/xenc+xml', + 'xer': 'application/patch-ops-error+xml', + 'xfdf': 'application/vnd.adobe.xfdf', + 'xfdl': 'application/vnd.xfdl', + 'xht': 'application/xhtml+xml', + 'xhtml': 'application/xhtml+xml', + 'xhvml': 'application/xv+xml', + 'xif': 'image/vnd.xiff', + 'xla': 'application/vnd.ms-excel', + 'xlam': 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlc': 'application/vnd.ms-excel', + 'xlf': 'application/x-xliff+xml', + 'xlm': 'application/vnd.ms-excel', + 'xls': 'application/vnd.ms-excel', + 'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12', + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt': 'application/vnd.ms-excel', + 'xltm': 'application/vnd.ms-excel.template.macroenabled.12', + 'xltx': + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw': 'application/vnd.ms-excel', + 'xm': 'audio/xm', + 'xml': 'application/xml', + 'xo': 'application/vnd.olpc-sugar', + 'xop': 'application/xop+xml', + 'xpi': 'application/x-xpinstall', + 'xpl': 'application/xproc+xml', + 'xpm': 'image/x-xpixmap', + 'xpr': 'application/vnd.is-xpr', + 'xps': 'application/vnd.ms-xpsdocument', + 'xpw': 'application/vnd.intercon.formnet', + 'xpx': 'application/vnd.intercon.formnet', + 'xsl': 'application/xml', + 'xslt': 'application/xslt+xml', + 'xsm': 'application/vnd.syncml+xml', + 'xspf': 'application/xspf+xml', + 'xul': 'application/vnd.mozilla.xul+xml', + 'xvm': 'application/xv+xml', + 'xvml': 'application/xv+xml', + 'xwd': 'image/x-xwindowdump', + 'xyz': 'chemical/x-xyz', + 'xz': 'application/x-xz', + 'yang': 'application/yang', + 'yin': 'application/yin+xml', + 'z1': 'application/x-zmachine', + 'z2': 'application/x-zmachine', + 'z3': 'application/x-zmachine', + 'z4': 'application/x-zmachine', + 'z5': 'application/x-zmachine', + 'z6': 'application/x-zmachine', + 'z7': 'application/x-zmachine', + 'z8': 'application/x-zmachine', + 'zaz': 'application/vnd.zzazz.deck+xml', + 'zip': 'application/zip', + 'zir': 'application/vnd.zul', + 'zirz': 'application/vnd.zul', + 'zmm': 'application/vnd.handheld-entertainment+xml', +}; diff --git a/pkgs/mime/lib/src/magic_number.dart b/pkgs/mime/lib/src/magic_number.dart new file mode 100644 index 00000000..c8b5c3be --- /dev/null +++ b/pkgs/mime/lib/src/magic_number.dart @@ -0,0 +1,407 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +class MagicNumber { + final String mimeType; + final List numbers; + final List? mask; + + const MagicNumber(this.mimeType, this.numbers, {this.mask}); + + bool matches(List header) { + if (header.length < numbers.length) return false; + + for (var i = 0; i < numbers.length; i++) { + if (mask != null) { + if ((mask![i] & numbers[i]) != (mask![i] & header[i])) return false; + } else { + if (numbers[i] != header[i]) return false; + } + } + + return true; + } +} + +const int initialMagicNumbersMaxLength = 12; + +const List initialMagicNumbers = [ + MagicNumber('application/pdf', [0x25, 0x50, 0x44, 0x46]), + MagicNumber('application/postscript', [0x25, 0x51]), + + /// AIFF is based on the EA IFF 85 Standard for Interchange Format Files. + /// -> 4 bytes have the ASCII characters 'F' 'O' 'R' 'M'. + /// -> 4 bytes indicating the size of the file + /// -> 4 bytes have the ASCII characters 'A' 'I' 'F' 'F'. + /// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/Docs/AIFF-1.3.pdf + MagicNumber('audio/x-aiff', [ + 0x46, + 0x4F, + 0x52, + 0x4D, + 0x00, + 0x00, + 0x00, + 0x00, + 0x41, + 0x49, + 0x46, + 0x46 + ], mask: [ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + + /// -> 4 bytes have the ASCII characters 'f' 'L' 'a' 'C'. + /// https://xiph.org/flac/format.html + MagicNumber('audio/x-flac', [0x66, 0x4C, 0x61, 0x43]), + + /// The WAVE file format is based on the RIFF document format. + /// -> 4 bytes have the ASCII characters 'R' 'I' 'F' 'F'. + /// -> 4 bytes indicating the size of the file + /// -> 4 bytes have the ASCII characters 'W' 'A' 'V' 'E'. + /// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf + MagicNumber('audio/x-wav', [ + 0x52, + 0x49, + 0x46, + 0x46, + 0x00, + 0x00, + 0x00, + 0x00, + 0x57, + 0x41, + 0x56, + 0x45 + ], mask: [ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('image/gif', [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]), + MagicNumber('image/gif', [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]), + MagicNumber('image/jpeg', [0xFF, 0xD8]), + MagicNumber('image/png', [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), + MagicNumber('image/tiff', [0x49, 0x49, 0x2A, 0x00]), + MagicNumber('image/tiff', [0x4D, 0x4D, 0x00, 0x2A]), + MagicNumber('audio/aac', [0xFF, 0xF1]), + MagicNumber('audio/aac', [0xFF, 0xF9]), + MagicNumber('audio/weba', [0x1A, 0x45, 0xDF, 0xA3]), + MagicNumber('audio/mpeg', [0x49, 0x44, 0x33]), + MagicNumber('audio/mpeg', [0xFF, 0xFB]), + MagicNumber('audio/ogg', [0x4F, 0x70, 0x75]), + MagicNumber('video/3gpp', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x33, + 0x67, + 0x70, + 0x35 + ], mask: [ + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('video/mp4', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x61, + 0x76, + 0x63, + 0x31 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('video/mp4', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x69, + 0x73, + 0x6F, + 0x32 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('video/mp4', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x69, + 0x73, + 0x6F, + 0x6D + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('video/mp4', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x70, + 0x34, + 0x31 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('video/mp4', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x70, + 0x34, + 0x32 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + MagicNumber('model/gltf-binary', [0x46, 0x54, 0x6C, 0x67]), + + /// The WebP file format is based on the RIFF document format. + /// -> 4 bytes have the ASCII characters 'R' 'I' 'F' 'F'. + /// -> 4 bytes indicating the size of the file + /// -> 4 bytes have the ASCII characters 'W' 'E' 'B' 'P'. + /// https://developers.google.com/speed/webp/docs/riff_container + MagicNumber('image/webp', [ + 0x52, + 0x49, + 0x46, + 0x46, + 0x00, + 0x00, + 0x00, + 0x00, + 0x57, + 0x45, + 0x42, + 0x50 + ], mask: [ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + + MagicNumber('font/woff2', [0x77, 0x4f, 0x46, 0x32]), + + /// High Efficiency Image File Format (ISO/IEC 23008-12). + /// -> 4 bytes indicating the ftyp box length. + /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. + /// -> 4 bytes have the ASCII characters 'h' 'e' 'i' 'c'. + /// https://www.iana.org/assignments/media-types/image/heic + MagicNumber('image/heic', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x68, + 0x65, + 0x69, + 0x63 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + + /// -> 4 bytes indicating the ftyp box length. + /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. + /// -> 4 bytes have the ASCII characters 'h' 'e' 'i' 'x'. + MagicNumber('image/heic', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x68, + 0x65, + 0x69, + 0x78 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), + + /// -> 4 bytes indicating the ftyp box length. + /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. + /// -> 4 bytes have the ASCII characters 'm' 'i' 'f' '1'. + MagicNumber('image/heif', [ + 0x00, + 0x00, + 0x00, + 0x00, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x69, + 0x66, + 0x31 + ], mask: [ + 0x00, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]), +]; diff --git a/pkgs/mime/lib/src/mime_multipart_transformer.dart b/pkgs/mime/lib/src/mime_multipart_transformer.dart new file mode 100644 index 00000000..2648d7d8 --- /dev/null +++ b/pkgs/mime/lib/src/mime_multipart_transformer.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import 'bound_multipart_stream.dart'; +import 'char_code.dart' as char_code; +import 'mime_shared.dart'; + +Uint8List _getBoundary(String boundary) { + final charCodes = boundary.codeUnits; + + final boundaryList = Uint8List(4 + charCodes.length); + // Set-up the matching boundary preceding it with CRLF and two + // dashes. + boundaryList[0] = char_code.cr; + boundaryList[1] = char_code.lf; + boundaryList[2] = char_code.dash; + boundaryList[3] = char_code.dash; + boundaryList.setRange(4, 4 + charCodes.length, charCodes); + return boundaryList; +} + +/// Parser for MIME multipart types of data as described in RFC 2046 +/// section 5.1.1. The data is transformed into [MimeMultipart] objects, each +/// of them streaming the multipart data. +class MimeMultipartTransformer + extends StreamTransformerBase, MimeMultipart> { + final List _boundary; + + /// Construct a new MIME multipart parser with the boundary + /// [boundary]. The boundary should be as specified in the content + /// type parameter, that is without the -- prefix. + MimeMultipartTransformer(String boundary) + : _boundary = _getBoundary(boundary); + + @override + Stream bind(Stream> stream) => + BoundMultipartStream(_boundary, stream).stream; +} diff --git a/pkgs/mime/lib/src/mime_shared.dart b/pkgs/mime/lib/src/mime_shared.dart new file mode 100644 index 00000000..1c98971e --- /dev/null +++ b/pkgs/mime/lib/src/mime_shared.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'mime_multipart_transformer.dart'; + +class MimeMultipartException implements Exception { + final String message; + + const MimeMultipartException([this.message = '']); + + @override + String toString() => 'MimeMultipartException: $message'; +} + +/// A Mime Multipart class representing each part parsed by +/// [MimeMultipartTransformer]. The data is streamed in as it become available. +abstract class MimeMultipart extends Stream> { + Map get headers; +} diff --git a/pkgs/mime/lib/src/mime_type.dart b/pkgs/mime/lib/src/mime_type.dart new file mode 100644 index 00000000..03121c97 --- /dev/null +++ b/pkgs/mime/lib/src/mime_type.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'default_extension_map.dart'; +import 'magic_number.dart'; + +final MimeTypeResolver _globalResolver = MimeTypeResolver(); + +/// The maximum number of bytes needed, to match all default magic-numbers. +int get defaultMagicNumbersMaxLength => _globalResolver.magicNumbersMaxLength; + +/// Extract the extension from [path] and use that for MIME-type lookup, using +/// the default extension map. +/// +/// If no matching MIME-type was found, `null` is returned. +/// +/// If [headerBytes] is present, a match for known magic-numbers will be +/// performed first. This allows the correct mime-type to be found, even though +/// a file have been saved using the wrong file-name extension. If less than +/// [defaultMagicNumbersMaxLength] bytes was provided, some magic-numbers won't +/// be matched against. +String? lookupMimeType(String path, {List? headerBytes}) => + _globalResolver.lookup(path, headerBytes: headerBytes); + +/// Returns the extension for the given MIME type. +/// +/// If there are multiple extensions for [mime], return the first occurrence in +/// the map. If there are no extensions for [mime], return [mime]. +String extensionFromMime(String mime) { + mime = mime.toLowerCase(); + for (final entry in defaultExtensionMap.entries) { + if (defaultExtensionMap[entry.key] == mime) { + return entry.key; + } + } + return mime; +} + +/// MIME-type resolver class, used to customize the lookup of mime-types. +class MimeTypeResolver { + final Map _extensionMap = {}; + final List _magicNumbers = []; + final bool _useDefault; + int _magicNumbersMaxLength; + + /// Create a new empty [MimeTypeResolver]. + MimeTypeResolver.empty() + : _useDefault = false, + _magicNumbersMaxLength = 0; + + /// Create a new [MimeTypeResolver] containing the default scope. + MimeTypeResolver() + : _useDefault = true, + _magicNumbersMaxLength = initialMagicNumbersMaxLength; + + /// Get the maximum number of bytes required to match all magic numbers, when + /// performing [lookup] with headerBytes present. + int get magicNumbersMaxLength => _magicNumbersMaxLength; + + /// Extract the extension from [path] and use that for MIME-type lookup. + /// + /// If no matching MIME-type was found, `null` is returned. + /// + /// If [headerBytes] is present, a match for known magic-numbers will be + /// performed first. This allows the correct mime-type to be found, even + /// though a file have been saved using the wrong file-name extension. If less + /// than [magicNumbersMaxLength] bytes was provided, some magic-numbers won't + /// be matched against. + String? lookup(String path, {List? headerBytes}) { + String? result; + if (headerBytes != null) { + result = _matchMagic(headerBytes, _magicNumbers); + if (result != null) return result; + if (_useDefault) { + result = _matchMagic(headerBytes, initialMagicNumbers); + if (result != null) return result; + } + } + final ext = _ext(path); + result = _extensionMap[ext]; + if (result != null) return result; + if (_useDefault) { + result = defaultExtensionMap[ext]; + if (result != null) return result; + } + return null; + } + + /// Add a new MIME-type mapping to the [MimeTypeResolver]. If the [extension] + /// is already present in the [MimeTypeResolver], it'll be overwritten. + void addExtension(String extension, String mimeType) { + _extensionMap[extension] = mimeType; + } + + /// Add a new magic-number mapping to the [MimeTypeResolver]. + /// + /// If [mask] is present,the [mask] is used to only perform matching on + /// selective bits. The [mask] must have the same length as [bytes]. + void addMagicNumber(List bytes, String mimeType, {List? mask}) { + if (mask != null && bytes.length != mask.length) { + throw ArgumentError('Bytes and mask are of different lengths'); + } + if (bytes.length > _magicNumbersMaxLength) { + _magicNumbersMaxLength = bytes.length; + } + _magicNumbers.add(MagicNumber(mimeType, bytes, mask: mask)); + } + + static String? _matchMagic( + List headerBytes, List magicNumbers) { + for (var mn in magicNumbers) { + if (mn.matches(headerBytes)) return mn.mimeType; + } + return null; + } + + static String _ext(String path) { + final index = path.lastIndexOf('.'); + if (index < 0 || index + 1 >= path.length) return path; + return path.substring(index + 1).toLowerCase(); + } +} diff --git a/pkgs/mime/pubspec.yaml b/pkgs/mime/pubspec.yaml new file mode 100644 index 00000000..9e20caa9 --- /dev/null +++ b/pkgs/mime/pubspec.yaml @@ -0,0 +1,18 @@ +name: mime +version: 1.0.6-wip +description: >- + Utilities for handling media (MIME) types, including determining a type from + a file extension and file contents. +repository: https://github.com/dart-lang/mime +topics: + - magic-numbers + - mime + - mimetype + - multipart-form + +environment: + sdk: ^3.2.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.0 diff --git a/pkgs/mime/test/default_extension_map_test.dart b/pkgs/mime/test/default_extension_map_test.dart new file mode 100644 index 00000000..b5539dd4 --- /dev/null +++ b/pkgs/mime/test/default_extension_map_test.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:mime/src/default_extension_map.dart'; +import 'package:test/test.dart'; + +void main() { + group('defaultExtensionMap', () { + test('keys are lowercase', () { + for (final key in defaultExtensionMap.keys) { + expect(key, equals(key.toLowerCase())); + } + }); + }); +} diff --git a/pkgs/mime/test/mime_multipart_transformer_test.dart b/pkgs/mime/test/mime_multipart_transformer_test.dart new file mode 100644 index 00000000..109a9bcd --- /dev/null +++ b/pkgs/mime/test/mime_multipart_transformer_test.dart @@ -0,0 +1,469 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:mime/mime.dart'; +import 'package:test/test.dart'; + +void _writeInChunks( + List data, int chunkSize, StreamController> controller) { + if (chunkSize == -1) chunkSize = data.length; + + for (var pos = 0; pos < data.length; pos += chunkSize) { + final remaining = data.length - pos; + final writeLength = min(chunkSize, remaining); + controller.add(data.sublist(pos, pos + writeLength)); + } + controller.close(); +} + +enum TestMode { immediateListen, delayListen, pauseResume } + +void _runParseTest( + String message, + String boundary, + TestMode mode, [ + List>? expectedHeaders, + List? expectedParts, + bool expectError = false, +]) { + Future testWrite(List data, [int chunkSize = -1]) { + final controller = StreamController>(sync: true); + + final stream = + controller.stream.transform(MimeMultipartTransformer(boundary)); + var i = 0; + final completer = Completer(); + final futures = >[]; + stream.listen((multipart) { + final part = i++; + if (expectedHeaders != null) { + expect(multipart.headers, equals(expectedHeaders[part])); + } + switch (mode) { + case TestMode.immediateListen: + futures.add(multipart.fold>( + [], (buffer, data) => buffer..addAll(data)).then((data) { + if (expectedParts?[part] != null) { + expect(data, equals(expectedParts?[part]!.codeUnits)); + } + })); + + case TestMode.delayListen: + futures.add( + Future( + () => multipart.fold>( + [], + (buffer, data) => buffer..addAll(data), + ).then( + (data) { + if (expectedParts?[part] != null) { + expect(data, equals(expectedParts?[part]!.codeUnits)); + } + }, + ), + ), + ); + + case TestMode.pauseResume: + final completer = Completer(); + futures.add(completer.future); + final buffer = []; + late StreamSubscription> subscription; + subscription = multipart.listen((data) { + buffer.addAll(data); + subscription.pause(); + Future(() => subscription.resume()); + }, onDone: () { + if (expectedParts?[part] != null) { + expect(buffer, equals(expectedParts?[part]!.codeUnits)); + } + completer.complete(); + }); + addTearDown(subscription.cancel); + } + }, onError: (Object error) { + // ignore: only_throw_errors + if (!expectError) throw error; + }, onDone: () { + if (expectedParts != null) { + expect(i, equals(expectedParts.length)); + } + Future.wait(futures).then(completer.complete); + }); + + _writeInChunks(data, chunkSize, controller); + + return completer.future; + } + + Future testFirstPartOnly(List data, [int chunkSize = -1]) { + final completer = Completer(); + final controller = StreamController>(sync: true); + + final stream = + controller.stream.transform(MimeMultipartTransformer(boundary)); + + stream.first.then((multipart) { + if (expectedHeaders != null) { + expect(multipart.headers, equals(expectedHeaders[0])); + } + return multipart.fold>([], (b, d) => b..addAll(d)).then( + (data) { + if (expectedParts != null && expectedParts[0] != null) { + expect(data, equals(expectedParts[0]!.codeUnits)); + } + }, + ); + }).then((_) { + completer.complete(); + }); + + _writeInChunks(data, chunkSize, controller); + + return completer.future; + } + + Future testCompletePartAfterCancel(List data, int parts, + [int chunkSize = -1]) { + final completer = Completer(); + final controller = StreamController>(sync: true); + final stream = + controller.stream.transform(MimeMultipartTransformer(boundary)); + late StreamSubscription subscription; + var i = 0; + final futures = >[]; + subscription = stream.listen((multipart) { + final partIndex = i; + + if (partIndex >= parts) { + throw StateError('Expected no more parts, but got one.'); + } + + if (expectedHeaders != null) { + expect(multipart.headers, equals(expectedHeaders[partIndex])); + } + futures.add( + multipart.fold>([], (b, d) => b..addAll(d)).then((data) { + if (expectedParts != null && expectedParts[partIndex] != null) { + expect(data, equals(expectedParts[partIndex]!.codeUnits)); + } + })); + + if (partIndex == (parts - 1)) { + subscription.cancel(); + Future.wait(futures).then(completer.complete); + } + i++; + }); + + _writeInChunks(data, chunkSize, controller); + + return completer.future; + } + + // Test parsing the data three times delivering the data in + // different chunks. + final data = message.codeUnits; + test('test', () { + expect( + Future.wait([ + testWrite(data), + testWrite(data, 10), + testWrite(data, 2), + testWrite(data, 1), + ]), + completes); + }); + + if (expectedParts!.isNotEmpty) { + test('test-first-part-only', () { + expect( + Future.wait([ + testFirstPartOnly(data), + testFirstPartOnly(data, 10), + testFirstPartOnly(data, 2), + testFirstPartOnly(data, 1), + ]), + completes); + }); + + test('test-n-parts-only', () { + var numPartsExpected = expectedParts.length - 1; + if (numPartsExpected == 0) numPartsExpected = 1; + + expect( + Future.wait([ + testCompletePartAfterCancel(data, numPartsExpected), + testCompletePartAfterCancel(data, numPartsExpected, 10), + testCompletePartAfterCancel(data, numPartsExpected, 2), + testCompletePartAfterCancel(data, numPartsExpected, 1), + ]), + completes); + }); + } +} + +void _testParse(String message, String boundary, + [List>? expectedHeaders, + List? expectedParts, + bool expectError = false]) { + _runParseTest(message, boundary, TestMode.immediateListen, expectedHeaders, + expectedParts, expectError); + _runParseTest(message, boundary, TestMode.delayListen, expectedHeaders, + expectedParts, expectError); + _runParseTest(message, boundary, TestMode.pauseResume, expectedHeaders, + expectedParts, expectError); +} + +void _testParseValid() { + // Empty message from Chrome form post. + var message = '------WebKitFormBoundaryU3FBruSkJKG0Yor1--\r\n'; + _testParse(message, '----WebKitFormBoundaryU3FBruSkJKG0Yor1', [], []); + + // Sample from Wikipedia. + message = ''' +This is a message with multiple parts in MIME format.\r +--frontier\r +Content-Type: text/plain\r +\r +This is the body of the message.\r +--frontier\r +Content-Type: application/octet-stream\r +Content-Transfer-Encoding: base64\r +\r +PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg +Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=\r +--frontier--\r\n'''; + var headers1 = {'content-type': 'text/plain'}; + var headers2 = { + 'content-type': 'application/octet-stream', + 'content-transfer-encoding': 'base64' + }; + var body1 = 'This is the body of the message.'; + var body2 = ''' +PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg +Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg='''; + _testParse(message, 'frontier', [headers1, headers2], [body1, body2]); + + // Sample from HTML 4.01 Specification. + message = ''' +\r\n--AaB03x\r +Content-Disposition: form-data; name="submit-name"\r +\r +Larry\r +--AaB03x\r +Content-Disposition: form-data; name="files"; filename="file1.txt"\r +Content-Type: text/plain\r +\r +... contents of file1.txt ...\r +--AaB03x--\r\n'''; + headers1 = { + 'content-disposition': 'form-data; name="submit-name"' + }; + headers2 = { + 'content-type': 'text/plain', + 'content-disposition': 'form-data; name="files"; filename="file1.txt"' + }; + body1 = 'Larry'; + body2 = '... contents of file1.txt ...'; + _testParse(message, 'AaB03x', [headers1, headers2], [body1, body2]); + + // Longer form from submitting the following from Chrome. + // + // + // + //
+ //

+ // Text: + // Password: + // Checkbox: + // Radio: + // Send + //

+ //
+ // + // + + message = ''' +\r\n------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r +Content-Disposition: form-data; name="text_input"\r +\r +text\r +------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r +Content-Disposition: form-data; name="password_input"\r +\r +password\r +------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r +Content-Disposition: form-data; name="checkbox_input"\r +\r +on\r +------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r +Content-Disposition: form-data; name="radio_input"\r +\r +on\r +------WebKitFormBoundaryQ3cgYAmGRF8yOeYB--\r\n'''; + headers1 = { + 'content-disposition': 'form-data; name="text_input"' + }; + headers2 = { + 'content-disposition': 'form-data; name="password_input"' + }; + final headers3 = { + 'content-disposition': 'form-data; name="checkbox_input"' + }; + final headers4 = { + 'content-disposition': 'form-data; name="radio_input"' + }; + body1 = 'text'; + body2 = 'password'; + const body3 = 'on'; + const body4 = 'on'; + _testParse(message, '----WebKitFormBoundaryQ3cgYAmGRF8yOeYB', + [headers1, headers2, headers3, headers4], [body1, body2, body3, body4]); + + // Same form from Firefox. + message = ''' +\r\n-----------------------------52284550912143824192005403738\r +Content-Disposition: form-data; name="text_input"\r +\r +text\r +-----------------------------52284550912143824192005403738\r +Content-Disposition: form-data; name="password_input"\r +\r +password\r +-----------------------------52284550912143824192005403738\r +Content-Disposition: form-data; name="checkbox_input"\r +\r +on\r +-----------------------------52284550912143824192005403738\r +Content-Disposition: form-data; name="radio_input"\r +\r +on\r +-----------------------------52284550912143824192005403738--\r\n'''; + _testParse( + message, + '---------------------------52284550912143824192005403738', + [headers1, headers2, headers3, headers4], + [body1, body2, body3, body4]); + + // And Internet Explorer + message = ''' +\r\n-----------------------------7dc8f38c60326\r +Content-Disposition: form-data; name="text_input"\r +\r +text\r +-----------------------------7dc8f38c60326\r +Content-Disposition: form-data; name="password_input"\r +\r +password\r +-----------------------------7dc8f38c60326\r +Content-Disposition: form-data; name="checkbox_input"\r +\r +on\r +-----------------------------7dc8f38c60326\r +Content-Disposition: form-data; name="radio_input"\r +\r +on\r +-----------------------------7dc8f38c60326--\r\n'''; + _testParse(message, '---------------------------7dc8f38c60326', + [headers1, headers2, headers3, headers4], [body1, body2, body3, body4]); + + // Test boundary prefix inside prefix and content. + message = ''' +-\r +--\r +--b\r +--bo\r +--bou\r +--boun\r +--bound\r +--bounda\r +--boundar\r +--boundary\r +Content-Type: text/plain\r +\r +-\r +--\r +--b\r +--bo\r +--bou\r +--boun\r +--bound\r\r +--bounda\r\r\r +--boundar\r\r\r\r +--boundary\r +Content-Type: text/plain\r +\r +--boundar\r +--bounda\r +--bound\r +--boun\r +--bou\r +--bo\r +--b\r\r\r\r +--\r\r\r +-\r\r +--boundary--\r\n'''; + final headers = {'content-type': 'text/plain'}; + body1 = ''' +-\r +--\r +--b\r +--bo\r +--bou\r +--boun\r +--bound\r\r +--bounda\r\r\r +--boundar\r\r\r'''; + body2 = ''' +--boundar\r +--bounda\r +--bound\r +--boun\r +--bou\r +--bo\r +--b\r\r\r\r +--\r\r\r +-\r'''; + _testParse(message, 'boundary', [headers, headers], [body1, body2]); + + // Without initial CRLF. + message = ''' +--xxx\r +\r +\r +Body 1\r +--xxx\r +\r +\r +Body2\r +--xxx--\r\n'''; + _testParse(message, 'xxx', null, ['\r\nBody 1', '\r\nBody2']); +} + +void _testParseInvalid() { + // Missing end boundary. + const message = ''' +\r +--xxx\r +\r +\r +Body 1\r +--xxx\r +\r +\r +Body2\r +--xxx\r\n'''; + _testParse(message, 'xxx', null, [null, null], true); +} + +void main() { + _testParseValid(); + _testParseInvalid(); +} diff --git a/pkgs/mime/test/mime_type_test.dart b/pkgs/mime/test/mime_type_test.dart new file mode 100644 index 00000000..f7388ff3 --- /dev/null +++ b/pkgs/mime/test/mime_type_test.dart @@ -0,0 +1,338 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:mime/mime.dart'; +import 'package:mime/src/magic_number.dart'; +import 'package:test/test.dart'; + +void _expectMimeType(String path, String? expectedMimeType, + {List? headerBytes, MimeTypeResolver? resolver}) { + String? mimeType; + if (resolver == null) { + mimeType = lookupMimeType(path, headerBytes: headerBytes); + } else { + mimeType = resolver.lookup(path, headerBytes: headerBytes); + } + + expect(mimeType, expectedMimeType); +} + +void main() { + group('global-lookup', () { + test('by-path', () { + _expectMimeType('file.dart', 'text/x-dart'); + // Test mixed-case + _expectMimeType('file.DaRT', 'text/x-dart'); + _expectMimeType('file.dcm', 'application/dicom'); + _expectMimeType('file.html', 'text/html'); + _expectMimeType('file.xhtml', 'application/xhtml+xml'); + _expectMimeType('file.jpeg', 'image/jpeg'); + _expectMimeType('file.jpg', 'image/jpeg'); + _expectMimeType('file.png', 'image/png'); + _expectMimeType('file.gif', 'image/gif'); + _expectMimeType('file.cc', 'text/x-c'); + _expectMimeType('file.c', 'text/x-c'); + _expectMimeType('file.css', 'text/css'); + _expectMimeType('file.js', 'text/javascript'); + _expectMimeType('file.mjs', 'text/javascript'); + _expectMimeType('file.ps', 'application/postscript'); + _expectMimeType('file.pdf', 'application/pdf'); + _expectMimeType('file.tiff', 'image/tiff'); + _expectMimeType('file.tif', 'image/tiff'); + _expectMimeType('file.webp', 'image/webp'); + _expectMimeType('file.mp3', 'audio/mpeg'); + _expectMimeType('file.aac', 'audio/aac'); + _expectMimeType('file.ogg', 'audio/ogg'); + _expectMimeType('file.aiff', 'audio/x-aiff'); + _expectMimeType('file.m4a', 'audio/mp4'); + _expectMimeType('file.m4b', 'audio/mp4'); + _expectMimeType('file.toml', 'application/toml'); + _expectMimeType('file.md', 'text/markdown'); + _expectMimeType('file.markdown', 'text/markdown'); + _expectMimeType('file.heif', 'image/heif'); + _expectMimeType('file.heic', 'image/heic'); + }); + + test('unknown-mime-type', () { + _expectMimeType('file.unsupported-extension', null); + }); + + test('by-header-bytes', () { + _expectMimeType('file.jpg', 'image/png', + headerBytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + _expectMimeType('file.jpg', 'image/gif', headerBytes: [ + 0x47, + 0x49, + 0x46, + 0x38, + 0x39, + 0x61, + 0x0D, + 0x0A, + 0x1A, + 0x0A + ]); + _expectMimeType('file.gif', 'image/jpeg', headerBytes: [ + 0xFF, + 0xD8, + 0x46, + 0x38, + 0x39, + 0x61, + 0x0D, + 0x0A, + 0x1A, + 0x0A + ]); + _expectMimeType('file', 'video/3gpp', headerBytes: [ + 0x00, + 0x00, + 0x00, + 0x04, + 0x66, + 0x74, + 0x79, + 0x70, + 0x33, + 0x67, + 0x70, + 0x35 + ]); + _expectMimeType('file.mp4', 'video/mp4', headerBytes: [ + 0x00, + 0x00, + 0x00, + 0x04, + 0x66, + 0x74, + 0x79, + 0x70, + 0xFF, + 0xFF, + 0xFF, + 0xFF + ]); + _expectMimeType('file', 'video/mp4', headerBytes: [ + 0x00, + 0xF0, + 0xF0, + 0xF0, + 0x66, + 0x74, + 0x79, + 0x70, + 0x61, + 0x76, + 0x63, + 0x31 + ]); + _expectMimeType('file', 'video/mp4', headerBytes: [ + 0x00, + 0xF0, + 0xF0, + 0xF0, + 0x66, + 0x74, + 0x79, + 0x70, + 0x69, + 0x73, + 0x6F, + 0x32 + ]); + _expectMimeType('file', 'video/mp4', headerBytes: [ + 0x00, + 0xF0, + 0xF0, + 0xF0, + 0x66, + 0x74, + 0x79, + 0x70, + 0x69, + 0x73, + 0x6F, + 0x6D + ]); + _expectMimeType('file', 'video/mp4', headerBytes: [ + 0x00, + 0xF0, + 0xF0, + 0xF0, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x70, + 0x34, + 0x31 + ]); + _expectMimeType('file', 'video/mp4', headerBytes: [ + 0x00, + 0xF0, + 0xF0, + 0xF0, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x70, + 0x34, + 0x32 + ]); + _expectMimeType('file', 'image/webp', headerBytes: [ + 0x52, + 0x49, + 0x46, + 0x46, + 0xE2, + 0x4A, + 0x01, + 0x00, + 0x57, + 0x45, + 0x42, + 0x50 + ]); + _expectMimeType('file', 'audio/mpeg', + headerBytes: [0x49, 0x44, 0x33, 0x0D, 0x0A, 0x1A, 0x0A]); + _expectMimeType('file', 'audio/aac', + headerBytes: [0xFF, 0xF1, 0x0D, 0x0A, 0x1A, 0x0A]); + _expectMimeType('file', 'audio/ogg', + headerBytes: [0x4F, 0x70, 0x75, 0x0D, 0x0A, 0x1A, 0x0A]); + _expectMimeType('file', 'audio/x-aiff', headerBytes: [ + 0x46, + 0x4F, + 0x52, + 0x4D, + 0x04, + 0x0B, + 0xEF, + 0xF4, + 0x41, + 0x49, + 0x46, + 0x46 + ]); + _expectMimeType('file', 'audio/x-flac', + headerBytes: [0x66, 0x4C, 0x61, 0x43]); + _expectMimeType('file', 'audio/x-wav', headerBytes: [ + 0x52, + 0x49, + 0x46, + 0x46, + 0xA6, + 0x4E, + 0x70, + 0x03, + 0x57, + 0x41, + 0x56, + 0x45 + ]); + _expectMimeType('file', 'image/heic', headerBytes: [ + 0x00, + 0x00, + 0x00, + 0x18, + 0x66, + 0x74, + 0x79, + 0x70, + 0x68, + 0x65, + 0x69, + 0x63, + 0x00 + ]); + _expectMimeType('file', 'image/heic', headerBytes: [ + 0x00, + 0x00, + 0x00, + 0x18, + 0x66, + 0x74, + 0x79, + 0x70, + 0x68, + 0x65, + 0x69, + 0x78, + 0x00 + ]); + _expectMimeType('file', 'image/heif', headerBytes: [ + 0x00, + 0x00, + 0x00, + 0x18, + 0x66, + 0x74, + 0x79, + 0x70, + 0x6D, + 0x69, + 0x66, + 0x31, + 0x00 + ]); + }); + }); + + group('custom-resolver', () { + test('override-extension', () { + final resolver = MimeTypeResolver(); + resolver.addExtension('jpg', 'my-mime-type'); + _expectMimeType('file.jpg', 'my-mime-type', resolver: resolver); + }); + + test('fallthrough-extension', () { + final resolver = MimeTypeResolver(); + resolver.addExtension('jpg2', 'my-mime-type'); + _expectMimeType('file.jpg', 'image/jpeg', resolver: resolver); + }); + + test('with-mask', () { + final resolver = MimeTypeResolver.empty(); + resolver.addMagicNumber([0x01, 0x02, 0x03], 'my-mime-type', + mask: [0x01, 0xFF, 0xFE]); + _expectMimeType('file', 'my-mime-type', + headerBytes: [0x01, 0x02, 0x03], resolver: resolver); + _expectMimeType('file', null, + headerBytes: [0x01, 0x03, 0x03], resolver: resolver); + _expectMimeType('file', 'my-mime-type', + headerBytes: [0xFF, 0x02, 0x02], resolver: resolver); + }); + }); + + test('default magic number', () { + final actualMaxBytes = initialMagicNumbers.fold( + 0, + (previous, magic) => math.max(previous, magic.numbers.length), + ); + + expect(initialMagicNumbersMaxLength, actualMaxBytes); + }); + + group('extensionFromMime', () { + test('returns match for mime with single extension', () { + expect(extensionFromMime('application/json'), equals('json')); + expect(extensionFromMime('application/java-archive'), equals('jar')); + }); + + test('returns first match for mime with multiple extensions', () { + expect(extensionFromMime('text/html'), equals('htm')); + expect(extensionFromMime('application/x-cbr'), equals('cb7')); + }); + + test('returns inputted string for unrecognized mime', () { + expect( + extensionFromMime('unrecognized_mime'), equals('unrecognized_mime')); + expect(extensionFromMime('i/am/not/a/mime'), equals('i/am/not/a/mime')); + }); + }); +}