├── .github ├── dependabot.yaml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib ├── mime.dart └── src │ ├── bound_multipart_stream.dart │ ├── char_code.dart │ ├── default_extension_map.dart │ ├── magic_number.dart │ ├── mime_multipart_transformer.dart │ ├── mime_shared.dart │ └── mime_type.dart ├── pubspec.yaml └── test ├── default_extension_map_test.dart ├── mime_multipart_transformer_test.dart └── mime_type_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | schedule: 5 | # “At 00:00 (UTC) on Sunday.” 6 | - cron: '0 0 * * 0' 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | sdk: [3.2, dev] 20 | steps: 21 | # These are the latest versions of the github actions; dependabot will 22 | # send PRs to keep these up-to-date. 23 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 24 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 25 | 26 | - name: Install dependencies 27 | run: dart pub get 28 | 29 | - name: Verify formatting 30 | run: dart format --output=none --set-exit-if-changed . 31 | 32 | - name: Analyze project source 33 | run: dart analyze --fatal-infos 34 | 35 | - name: Run tests 36 | run: dart test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .dart_tool/ 3 | .pub/ 4 | packages 5 | pubspec.lock 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.6-wip 2 | 3 | * Add `topics` section to `pubspec.yaml`. 4 | 5 | ## 1.0.5 6 | 7 | * Update `video/mp4` mimeType lookup by header bytes. 8 | * Add `image/heic` mimeType lookup by header bytes. 9 | * Add `image/heif` mimeType lookup by header bytes. 10 | * Add m4b mimeType lookup by extension. 11 | * Add `text/markdown` mimeType lookup by extension. 12 | * Require Dart 3.2.0. 13 | 14 | ## 1.0.4 15 | 16 | * Changed `.js` to `text/javascript` per 17 | https://datatracker.ietf.org/doc/html/rfc9239. 18 | * Added `.mjs` as `text/javascript`. 19 | * Add `application/dicom` mimeType lookup by extension. 20 | * Require Dart 2.18. 21 | 22 | ## 1.0.3 23 | 24 | * Add application/manifest+json lookup by extension. 25 | * Add application/toml mimeType lookup by extension. 26 | * Add audio/aac mimeType lookup by header bytes. 27 | * Add audio/mpeg mimeType lookup by header bytes. 28 | * Add audio/ogg mimeType lookup by header bytes. 29 | * Add audio/weba mimeType lookup by header bytes. 30 | * Add font/woff2 lookup by extension and header bytes. 31 | * Add image/avif mimeType lookup by extension. 32 | * Add image/heic mimeType lookup by extension. 33 | * Add image/heif mimeType lookup by extension. 34 | * Change audio/x-aac to audio/aac when detected by extension. 35 | 36 | ## 1.0.2 37 | 38 | * Add audio/x-aiff mimeType lookup by header bytes. 39 | * Add audio/x-flac mimeType lookup by header bytes. 40 | * Add audio/x-wav mimeType lookup by header bytes. 41 | * Add audio/mp4 mimeType lookup by file path. 42 | 43 | ## 1.0.1 44 | 45 | * Add image/webp mimeType lookup by header bytes. 46 | 47 | ## 1.0.0 48 | 49 | * Stable null safety release. 50 | 51 | ## 1.0.0-nullsafety.0 52 | 53 | * Update to null safety. 54 | 55 | ## 0.9.7 56 | 57 | * Add `extensionFromMime` utility function. 58 | 59 | ## 0.9.6+3 60 | 61 | * Change the mime type for Dart source from `application/dart` to `text/x-dart`. 62 | * Add example. 63 | * Fix links and code in README. 64 | 65 | ## 0.9.6+2 66 | 67 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 68 | 69 | ## 0.9.6+1 70 | 71 | * Stop using deprecated constants from the SDK. 72 | 73 | ## 0.9.6 74 | 75 | * Updates to support Dart 2.0 core library changes (wave 76 | 2.2). See [issue 31847][sdk#31847] for details. 77 | 78 | [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847 79 | 80 | ## 0.9.5 81 | 82 | * Add support for the WebAssembly format. 83 | 84 | ## 0.9.4 85 | 86 | * Updated Dart SDK requirement to `>= 1.8.3 <2.0.0` 87 | 88 | * Strong-mode clean. 89 | 90 | * Added support for glTF text and binary formats. 91 | 92 | ## 0.9.3 93 | 94 | * Fixed erroneous behavior for listening and when pausing/resuming 95 | stream of parts. 96 | 97 | ## 0.9.2 98 | 99 | * Fixed erroneous behavior when pausing/canceling stream of parts but already 100 | listened to one part. 101 | 102 | ## 0.9.1 103 | 104 | * Handle parsing of MIME multipart content with no parts. 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/tools/tree/main/pkgs/mime 3 | 4 | [![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) 5 | [![Pub Package](https://img.shields.io/pub/v/mime.svg)](https://pub.dev/packages/mime) 6 | [![package publisher](https://img.shields.io/pub/publisher/mime.svg)](https://pub.dev/packages/mime/publisher) 7 | 8 | Package for working with MIME type definitions and for processing 9 | streams of MIME multipart media types. 10 | 11 | ## Determining the MIME type for a file 12 | 13 | The `MimeTypeResolver` class can be used to determine the MIME type of 14 | a file. It supports both using the extension of the file name and 15 | looking at magic bytes from the beginning of the file. 16 | 17 | There is a builtin instance of `MimeTypeResolver` accessible through 18 | the top level function `lookupMimeType`. This builtin instance has 19 | the most common file name extensions and magic bytes registered. 20 | 21 | ```dart 22 | import 'package:mime/mime.dart'; 23 | 24 | void main() { 25 | print(lookupMimeType('test.html')); 26 | // text/html 27 | 28 | print(lookupMimeType('test', headerBytes: [0xFF, 0xD8])); 29 | // image/jpeg 30 | 31 | print(lookupMimeType('test.html', headerBytes: [0xFF, 0xD8])); 32 | // image/jpeg 33 | } 34 | ``` 35 | 36 | You can build you own resolver by creating an instance of 37 | `MimeTypeResolver` and adding file name extensions and magic bytes 38 | using `addExtension` and `addMagicNumber`. 39 | 40 | ## Processing MIME multipart media types 41 | 42 | The class `MimeMultipartTransformer` is used to process a `Stream` of 43 | bytes encoded using a MIME multipart media types encoding. The 44 | transformer provides a new `Stream` of `MimeMultipart` objects each of 45 | which have the headers and the content of each part. The content of a 46 | part is provided as a stream of bytes. 47 | 48 | Below is an example showing how to process an HTTP request and print 49 | the length of the content of each part. 50 | 51 | ```dart 52 | // HTTP request with content type multipart/form-data. 53 | HttpRequest request = ...; 54 | // Determine the boundary form the content type header 55 | String boundary = request.headers.contentType.parameters['boundary']; 56 | 57 | // Process the body just calculating the length of each part. 58 | request 59 | .transform(new MimeMultipartTransformer(boundary)) 60 | .map((part) => part.fold(0, (p, d) => p + d)) 61 | .listen((length) => print('Part with length $length')); 62 | ``` 63 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/tools/analysis#the-analysis-options-file 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - avoid_bool_literals_in_conditional_expressions 13 | - avoid_classes_with_only_static_members 14 | - avoid_private_typedef_functions 15 | - avoid_redundant_argument_values 16 | - avoid_returning_this 17 | - avoid_unused_constructor_parameters 18 | - avoid_void_async 19 | - cancel_subscriptions 20 | - join_return_with_assignment 21 | - literal_only_boolean_expressions 22 | - missing_whitespace_between_adjacent_strings 23 | - no_adjacent_strings_in_list 24 | - no_runtimeType_toString 25 | - package_api_docs 26 | - prefer_const_declarations 27 | - prefer_expression_function_bodies 28 | - prefer_final_locals 29 | - unnecessary_await_in_return 30 | - unnecessary_breaks 31 | - unnecessary_raw_strings 32 | - use_if_null_to_convert_nulls_to_bools 33 | - use_raw_strings 34 | - use_string_buffers 35 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:mime/mime.dart'; 2 | 3 | void main() { 4 | print(lookupMimeType('test.html')); 5 | // text/html 6 | 7 | print(lookupMimeType('test', headerBytes: [0xFF, 0xD8])); 8 | // image/jpeg 9 | 10 | print(lookupMimeType('test.html', headerBytes: [0xFF, 0xD8])); 11 | // image/jpeg 12 | } 13 | -------------------------------------------------------------------------------- /lib/mime.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Help for working with file format identifiers 6 | /// such as `text/html` and `image/png`. 7 | /// 8 | /// More details, including a list of types, are in the Wikipedia article 9 | /// [Internet media type](http://en.wikipedia.org/wiki/Internet_media_type). 10 | library; 11 | 12 | export 'src/mime_multipart_transformer.dart'; 13 | export 'src/mime_shared.dart'; 14 | export 'src/mime_type.dart'; 15 | -------------------------------------------------------------------------------- /lib/src/bound_multipart_stream.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | import 'char_code.dart' as char_code; 9 | import 'mime_shared.dart'; 10 | 11 | /// Bytes for `()<>@,;:\\"/[]?={} \t`. 12 | const _separators = { 13 | 40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, 91, 93, 63, 61, 123, 125, 32, 9 // 14 | }; 15 | 16 | bool _isTokenChar(int byte) => 17 | byte > 31 && byte < 128 && !_separators.contains(byte); 18 | 19 | int _toLowerCase(int byte) { 20 | const delta = char_code.lowerA - char_code.upperA; 21 | return (char_code.upperA <= byte && byte <= char_code.upperZ) 22 | ? byte + delta 23 | : byte; 24 | } 25 | 26 | void _expectByteValue(int val1, int val2) { 27 | if (val1 != val2) { 28 | throw const MimeMultipartException('Failed to parse multipart mime 1'); 29 | } 30 | } 31 | 32 | void _expectWhitespace(int byte) { 33 | if (byte != char_code.sp && byte != char_code.ht) { 34 | throw const MimeMultipartException('Failed to parse multipart mime 2'); 35 | } 36 | } 37 | 38 | class _MimeMultipart extends MimeMultipart { 39 | @override 40 | final Map headers; 41 | final Stream> _stream; 42 | 43 | _MimeMultipart(this.headers, this._stream); 44 | 45 | @override 46 | StreamSubscription> listen( 47 | void Function(List data)? onData, { 48 | void Function()? onDone, 49 | Function? onError, 50 | bool? cancelOnError, 51 | }) => 52 | _stream.listen( 53 | onData, 54 | onDone: onDone, 55 | onError: onError, 56 | cancelOnError: cancelOnError, 57 | ); 58 | } 59 | 60 | class BoundMultipartStream { 61 | static const int _startCode = 0; 62 | static const int _boundaryEndingCode = 1; 63 | static const int _boundaryEndCode = 2; 64 | static const int _headerStartCode = 3; 65 | static const int _headerFieldCode = 4; 66 | static const int _headerValueStartCode = 5; 67 | static const int _headerValueCode = 6; 68 | static const int _headerValueFoldingOrEndingCode = 7; 69 | static const int _headerValueFoldOrEndCode = 8; 70 | static const int _headerEndingCode = 9; 71 | static const int _contentCode = 10; 72 | static const int _lastBoundaryDash2Code = 11; 73 | static const int _lastBoundaryEndingCode = 12; 74 | static const int _lastBoundaryEndCode = 13; 75 | static const int _doneCode = 14; 76 | static const int _failCode = 15; 77 | 78 | final List _boundary; 79 | final List _headerField = []; 80 | final List _headerValue = []; 81 | 82 | // The following states belong to `_controller`, state changes will not be 83 | // immediately acted upon but rather only after the current 84 | // `_multipartController` is done. 85 | static const int _controllerStateIdle = 0; 86 | static const int _controllerStateActive = 1; 87 | static const int _controllerStatePaused = 2; 88 | static const int _controllerStateCanceled = 3; 89 | 90 | int _controllerState = _controllerStateIdle; 91 | 92 | final _controller = StreamController(sync: true); 93 | 94 | Stream get stream => _controller.stream; 95 | 96 | late StreamSubscription _subscription; 97 | 98 | StreamController>? _multipartController; 99 | Map? _headers; 100 | 101 | int _state = _startCode; 102 | int _boundaryIndex = 2; 103 | 104 | /// Current index into [_buffer]. 105 | /// 106 | /// If index is negative then it is the index into the artificial prefix of 107 | /// the boundary string. 108 | int _index = 0; 109 | List _buffer = _placeholderBuffer; 110 | 111 | BoundMultipartStream(this._boundary, Stream> stream) { 112 | _controller 113 | ..onPause = _pauseStream 114 | ..onResume = _resumeStream 115 | ..onCancel = () { 116 | _controllerState = _controllerStateCanceled; 117 | _tryPropagateControllerState(); 118 | } 119 | ..onListen = () { 120 | _controllerState = _controllerStateActive; 121 | _subscription = stream.listen((data) { 122 | assert(_buffer == _placeholderBuffer); 123 | _subscription.pause(); 124 | _buffer = data; 125 | _index = 0; 126 | _parse(); 127 | }, onDone: () { 128 | if (_state != _doneCode) { 129 | _controller 130 | .addError(const MimeMultipartException('Bad multipart ending')); 131 | } 132 | _controller.close(); 133 | }, onError: _controller.addError); 134 | }; 135 | } 136 | 137 | void _resumeStream() { 138 | assert(_controllerState == _controllerStatePaused); 139 | _controllerState = _controllerStateActive; 140 | _tryPropagateControllerState(); 141 | } 142 | 143 | void _pauseStream() { 144 | _controllerState = _controllerStatePaused; 145 | _tryPropagateControllerState(); 146 | } 147 | 148 | void _tryPropagateControllerState() { 149 | if (_multipartController == null) { 150 | switch (_controllerState) { 151 | case _controllerStateActive: 152 | if (_subscription.isPaused) _subscription.resume(); 153 | case _controllerStatePaused: 154 | if (!_subscription.isPaused) _subscription.pause(); 155 | case _controllerStateCanceled: 156 | _subscription.cancel(); 157 | default: 158 | throw StateError('This code should never be reached.'); 159 | } 160 | } 161 | } 162 | 163 | void _parse() { 164 | // Number of boundary bytes to artificially place before the supplied data. 165 | // The data to parse might be 'artificially' prefixed with a 166 | // partial match of the boundary. 167 | final boundaryPrefix = _boundaryIndex; 168 | // Position where content starts. Will be null if no known content 169 | // start exists. Will be negative of the content starts in the 170 | // boundary prefix. Will be zero or position if the content starts 171 | // in the current buffer. 172 | var contentStartIndex = 173 | _state == _contentCode && _boundaryIndex == 0 ? 0 : null; 174 | 175 | // Function to report content data for the current part. The data 176 | // reported is from the current content start index up til the 177 | // current index. As the data can be artificially prefixed with a 178 | // prefix of the boundary both the content start index and index 179 | // can be negative. 180 | void reportData() { 181 | if (contentStartIndex! < 0) { 182 | final contentLength = boundaryPrefix + _index - _boundaryIndex; 183 | if (contentLength <= boundaryPrefix) { 184 | _multipartController!.add(_boundary.sublist(0, contentLength)); 185 | } else { 186 | _multipartController!.add(_boundary.sublist(0, boundaryPrefix)); 187 | _multipartController! 188 | .add(_buffer.sublist(0, contentLength - boundaryPrefix)); 189 | } 190 | } else { 191 | final contentEndIndex = _index - _boundaryIndex; 192 | _multipartController! 193 | .add(_buffer.sublist(contentStartIndex, contentEndIndex)); 194 | } 195 | } 196 | 197 | while ( 198 | _index < _buffer.length && _state != _failCode && _state != _doneCode) { 199 | final byte = 200 | _index < 0 ? _boundary[boundaryPrefix + _index] : _buffer[_index]; 201 | switch (_state) { 202 | case _startCode: 203 | if (byte == _boundary[_boundaryIndex]) { 204 | _boundaryIndex++; 205 | if (_boundaryIndex == _boundary.length) { 206 | _state = _boundaryEndingCode; 207 | _boundaryIndex = 0; 208 | } 209 | } else { 210 | // Restart matching of the boundary. 211 | _index = _index - _boundaryIndex; 212 | _boundaryIndex = 0; 213 | } 214 | 215 | case _boundaryEndingCode: 216 | if (byte == char_code.cr) { 217 | _state = _boundaryEndCode; 218 | } else if (byte == char_code.dash) { 219 | _state = _lastBoundaryDash2Code; 220 | } else { 221 | _expectWhitespace(byte); 222 | } 223 | 224 | case _boundaryEndCode: 225 | _expectByteValue(byte, char_code.lf); 226 | _multipartController?.close(); 227 | if (_multipartController != null) { 228 | _multipartController = null; 229 | _tryPropagateControllerState(); 230 | } 231 | _state = _headerStartCode; 232 | 233 | case _headerStartCode: 234 | _headers = {}; 235 | if (byte == char_code.cr) { 236 | _state = _headerEndingCode; 237 | } else { 238 | // Start of new header field. 239 | _headerField.add(_toLowerCase(byte)); 240 | _state = _headerFieldCode; 241 | } 242 | 243 | case _headerFieldCode: 244 | if (byte == char_code.colon) { 245 | _state = _headerValueStartCode; 246 | } else { 247 | if (!_isTokenChar(byte)) { 248 | throw const MimeMultipartException('Invalid header field name'); 249 | } 250 | _headerField.add(_toLowerCase(byte)); 251 | } 252 | 253 | case _headerValueStartCode: 254 | if (byte == char_code.cr) { 255 | _state = _headerValueFoldingOrEndingCode; 256 | } else if (byte != char_code.sp && byte != char_code.ht) { 257 | // Start of new header value. 258 | _headerValue.add(byte); 259 | _state = _headerValueCode; 260 | } 261 | 262 | case _headerValueCode: 263 | if (byte == char_code.cr) { 264 | _state = _headerValueFoldingOrEndingCode; 265 | } else { 266 | _headerValue.add(byte); 267 | } 268 | 269 | case _headerValueFoldingOrEndingCode: 270 | _expectByteValue(byte, char_code.lf); 271 | _state = _headerValueFoldOrEndCode; 272 | 273 | case _headerValueFoldOrEndCode: 274 | if (byte == char_code.sp || byte == char_code.ht) { 275 | _state = _headerValueStartCode; 276 | } else { 277 | final headerField = utf8.decode(_headerField); 278 | final headerValue = utf8.decode(_headerValue); 279 | _headers![headerField.toLowerCase()] = headerValue; 280 | _headerField.clear(); 281 | _headerValue.clear(); 282 | if (byte == char_code.cr) { 283 | _state = _headerEndingCode; 284 | } else { 285 | // Start of new header field. 286 | _headerField.add(_toLowerCase(byte)); 287 | _state = _headerFieldCode; 288 | } 289 | } 290 | 291 | case _headerEndingCode: 292 | _expectByteValue(byte, char_code.lf); 293 | _multipartController = StreamController( 294 | sync: true, 295 | onListen: () { 296 | if (_subscription.isPaused) _subscription.resume(); 297 | }, 298 | onPause: _subscription.pause, 299 | onResume: _subscription.resume); 300 | _controller 301 | .add(_MimeMultipart(_headers!, _multipartController!.stream)); 302 | _headers = null; 303 | _state = _contentCode; 304 | contentStartIndex = _index + 1; 305 | 306 | case _contentCode: 307 | if (byte == _boundary[_boundaryIndex]) { 308 | _boundaryIndex++; 309 | if (_boundaryIndex == _boundary.length) { 310 | if (contentStartIndex != null) { 311 | _index++; 312 | reportData(); 313 | _index--; 314 | } 315 | _multipartController!.close(); 316 | _multipartController = null; 317 | _tryPropagateControllerState(); 318 | _boundaryIndex = 0; 319 | _state = _boundaryEndingCode; 320 | } 321 | } else { 322 | // Restart matching of the boundary. 323 | _index = _index - _boundaryIndex; 324 | contentStartIndex ??= _index; 325 | _boundaryIndex = 0; 326 | } 327 | 328 | case _lastBoundaryDash2Code: 329 | _expectByteValue(byte, char_code.dash); 330 | _state = _lastBoundaryEndingCode; 331 | 332 | case _lastBoundaryEndingCode: 333 | if (byte == char_code.cr) { 334 | _state = _lastBoundaryEndCode; 335 | } else { 336 | _expectWhitespace(byte); 337 | } 338 | 339 | case _lastBoundaryEndCode: 340 | _expectByteValue(byte, char_code.lf); 341 | _multipartController?.close(); 342 | if (_multipartController != null) { 343 | _multipartController = null; 344 | _tryPropagateControllerState(); 345 | } 346 | _state = _doneCode; 347 | 348 | default: 349 | // Should be unreachable. 350 | assert(false); 351 | } 352 | 353 | // Move to the next byte. 354 | _index++; 355 | } 356 | 357 | // Report any known content. 358 | if (_state == _contentCode && contentStartIndex != null) { 359 | reportData(); 360 | } 361 | 362 | // Resume if at end. 363 | if (_index == _buffer.length) { 364 | _buffer = _placeholderBuffer; 365 | _index = 0; 366 | _subscription.resume(); 367 | } 368 | } 369 | } 370 | 371 | // Used as a placeholder instead of having a nullable buffer. 372 | const _placeholderBuffer = []; 373 | -------------------------------------------------------------------------------- /lib/src/char_code.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | const int ht = 9; 6 | const int lf = 10; 7 | const int cr = 13; 8 | const int sp = 32; 9 | const int dash = 45; 10 | const int colon = 58; 11 | const int upperA = 65; 12 | const int upperZ = 90; 13 | const int lowerA = 97; 14 | -------------------------------------------------------------------------------- /lib/src/default_extension_map.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | const Map defaultExtensionMap = { 6 | '123': 'application/vnd.lotus-1-2-3', 7 | '3dml': 'text/vnd.in3d.3dml', 8 | '3ds': 'image/x-3ds', 9 | '3g2': 'video/3gpp2', 10 | '3gp': 'video/3gpp', 11 | '7z': 'application/x-7z-compressed', 12 | 'aab': 'application/x-authorware-bin', 13 | 'aac': 'audio/aac', 14 | 'aam': 'application/x-authorware-map', 15 | 'aas': 'application/x-authorware-seg', 16 | 'abw': 'application/x-abiword', 17 | 'ac': 'application/pkix-attr-cert', 18 | 'acc': 'application/vnd.americandynamics.acc', 19 | 'ace': 'application/x-ace-compressed', 20 | 'acu': 'application/vnd.acucobol', 21 | 'acutc': 'application/vnd.acucorp', 22 | 'adp': 'audio/adpcm', 23 | 'aep': 'application/vnd.audiograph', 24 | 'afm': 'application/x-font-type1', 25 | 'afp': 'application/vnd.ibm.modcap', 26 | 'ahead': 'application/vnd.ahead.space', 27 | 'ai': 'application/postscript', 28 | 'aif': 'audio/x-aiff', 29 | 'aifc': 'audio/x-aiff', 30 | 'aiff': 'audio/x-aiff', 31 | 'air': 'application/vnd.adobe.air-application-installer-package+zip', 32 | 'ait': 'application/vnd.dvb.ait', 33 | 'ami': 'application/vnd.amiga.ami', 34 | 'apk': 'application/vnd.android.package-archive', 35 | 'appcache': 'text/cache-manifest', 36 | 'application': 'application/x-ms-application', 37 | 'apr': 'application/vnd.lotus-approach', 38 | 'arc': 'application/x-freearc', 39 | 'asc': 'application/pgp-signature', 40 | 'asf': 'video/x-ms-asf', 41 | 'asm': 'text/x-asm', 42 | 'aso': 'application/vnd.accpac.simply.aso', 43 | 'asx': 'video/x-ms-asf', 44 | 'atc': 'application/vnd.acucorp', 45 | 'atom': 'application/atom+xml', 46 | 'atomcat': 'application/atomcat+xml', 47 | 'atomsvc': 'application/atomsvc+xml', 48 | 'atx': 'application/vnd.antix.game-component', 49 | 'au': 'audio/basic', 50 | 'avi': 'video/x-msvideo', 51 | 'avif': 'image/avif', 52 | 'aw': 'application/applixware', 53 | 'azf': 'application/vnd.airzip.filesecure.azf', 54 | 'azs': 'application/vnd.airzip.filesecure.azs', 55 | 'azw': 'application/vnd.amazon.ebook', 56 | 'bat': 'application/x-msdownload', 57 | 'bcpio': 'application/x-bcpio', 58 | 'bdf': 'application/x-font-bdf', 59 | 'bdm': 'application/vnd.syncml.dm+wbxml', 60 | 'bed': 'application/vnd.realvnc.bed', 61 | 'bh2': 'application/vnd.fujitsu.oasysprs', 62 | 'bin': 'application/octet-stream', 63 | 'blb': 'application/x-blorb', 64 | 'blorb': 'application/x-blorb', 65 | 'bmi': 'application/vnd.bmi', 66 | 'bmp': 'image/bmp', 67 | 'book': 'application/vnd.framemaker', 68 | 'box': 'application/vnd.previewsystems.box', 69 | 'boz': 'application/x-bzip2', 70 | 'bpk': 'application/octet-stream', 71 | 'btif': 'image/prs.btif', 72 | 'bz': 'application/x-bzip', 73 | 'bz2': 'application/x-bzip2', 74 | 'c': 'text/x-c', 75 | 'c11amc': 'application/vnd.cluetrust.cartomobile-config', 76 | 'c11amz': 'application/vnd.cluetrust.cartomobile-config-pkg', 77 | 'c4d': 'application/vnd.clonk.c4group', 78 | 'c4f': 'application/vnd.clonk.c4group', 79 | 'c4g': 'application/vnd.clonk.c4group', 80 | 'c4p': 'application/vnd.clonk.c4group', 81 | 'c4u': 'application/vnd.clonk.c4group', 82 | 'cab': 'application/vnd.ms-cab-compressed', 83 | 'caf': 'audio/x-caf', 84 | 'cap': 'application/vnd.tcpdump.pcap', 85 | 'car': 'application/vnd.curl.car', 86 | 'cat': 'application/vnd.ms-pki.seccat', 87 | 'cb7': 'application/x-cbr', 88 | 'cba': 'application/x-cbr', 89 | 'cbr': 'application/x-cbr', 90 | 'cbt': 'application/x-cbr', 91 | 'cbz': 'application/x-cbr', 92 | 'cc': 'text/x-c', 93 | 'cct': 'application/x-director', 94 | 'ccxml': 'application/ccxml+xml', 95 | 'cdbcmsg': 'application/vnd.contact.cmsg', 96 | 'cdf': 'application/x-netcdf', 97 | 'cdkey': 'application/vnd.mediastation.cdkey', 98 | 'cdmia': 'application/cdmi-capability', 99 | 'cdmic': 'application/cdmi-container', 100 | 'cdmid': 'application/cdmi-domain', 101 | 'cdmio': 'application/cdmi-object', 102 | 'cdmiq': 'application/cdmi-queue', 103 | 'cdx': 'chemical/x-cdx', 104 | 'cdxml': 'application/vnd.chemdraw+xml', 105 | 'cdy': 'application/vnd.cinderella', 106 | 'cer': 'application/pkix-cert', 107 | 'cfs': 'application/x-cfs-compressed', 108 | 'cgm': 'image/cgm', 109 | 'chat': 'application/x-chat', 110 | 'chm': 'application/vnd.ms-htmlhelp', 111 | 'chrt': 'application/vnd.kde.kchart', 112 | 'cif': 'chemical/x-cif', 113 | 'cii': 'application/vnd.anser-web-certificate-issue-initiation', 114 | 'cil': 'application/vnd.ms-artgalry', 115 | 'cla': 'application/vnd.claymore', 116 | 'class': 'application/java-vm', 117 | 'clkk': 'application/vnd.crick.clicker.keyboard', 118 | 'clkp': 'application/vnd.crick.clicker.palette', 119 | 'clkt': 'application/vnd.crick.clicker.template', 120 | 'clkw': 'application/vnd.crick.clicker.wordbank', 121 | 'clkx': 'application/vnd.crick.clicker', 122 | 'clp': 'application/x-msclip', 123 | 'cmc': 'application/vnd.cosmocaller', 124 | 'cmdf': 'chemical/x-cmdf', 125 | 'cml': 'chemical/x-cml', 126 | 'cmp': 'application/vnd.yellowriver-custom-menu', 127 | 'cmx': 'image/x-cmx', 128 | 'cod': 'application/vnd.rim.cod', 129 | 'com': 'application/x-msdownload', 130 | 'conf': 'text/plain', 131 | 'cpio': 'application/x-cpio', 132 | 'cpp': 'text/x-c', 133 | 'cpt': 'application/mac-compactpro', 134 | 'crd': 'application/x-mscardfile', 135 | 'crl': 'application/pkix-crl', 136 | 'crt': 'application/x-x509-ca-cert', 137 | 'cryptonote': 'application/vnd.rig.cryptonote', 138 | 'csh': 'application/x-csh', 139 | 'csml': 'chemical/x-csml', 140 | 'csp': 'application/vnd.commonspace', 141 | 'css': 'text/css', 142 | 'cst': 'application/x-director', 143 | 'csv': 'text/csv', 144 | 'cu': 'application/cu-seeme', 145 | 'curl': 'text/vnd.curl', 146 | 'cww': 'application/prs.cww', 147 | 'cxt': 'application/x-director', 148 | 'cxx': 'text/x-c', 149 | 'dae': 'model/vnd.collada+xml', 150 | 'daf': 'application/vnd.mobius.daf', 151 | 'dart': 'text/x-dart', 152 | 'dataless': 'application/vnd.fdsn.seed', 153 | 'davmount': 'application/davmount+xml', 154 | 'dbk': 'application/docbook+xml', 155 | 'dcm': 'application/dicom', 156 | 'dcr': 'application/x-director', 157 | 'dcurl': 'text/vnd.curl.dcurl', 158 | 'dd2': 'application/vnd.oma.dd2+xml', 159 | 'ddd': 'application/vnd.fujixerox.ddd', 160 | 'deb': 'application/x-debian-package', 161 | 'def': 'text/plain', 162 | 'deploy': 'application/octet-stream', 163 | 'der': 'application/x-x509-ca-cert', 164 | 'dfac': 'application/vnd.dreamfactory', 165 | 'dgc': 'application/x-dgc-compressed', 166 | 'dic': 'text/x-c', 167 | 'dir': 'application/x-director', 168 | 'dis': 'application/vnd.mobius.dis', 169 | 'dist': 'application/octet-stream', 170 | 'distz': 'application/octet-stream', 171 | 'djv': 'image/vnd.djvu', 172 | 'djvu': 'image/vnd.djvu', 173 | 'dll': 'application/x-msdownload', 174 | 'dmg': 'application/x-apple-diskimage', 175 | 'dmp': 'application/vnd.tcpdump.pcap', 176 | 'dms': 'application/octet-stream', 177 | 'dna': 'application/vnd.dna', 178 | 'doc': 'application/msword', 179 | 'docm': 'application/vnd.ms-word.document.macroenabled.12', 180 | 'docx': 181 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 182 | 'dot': 'application/msword', 183 | 'dotm': 'application/vnd.ms-word.template.macroenabled.12', 184 | 'dotx': 185 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 186 | 'dp': 'application/vnd.osgi.dp', 187 | 'dpg': 'application/vnd.dpgraph', 188 | 'dra': 'audio/vnd.dra', 189 | 'dsc': 'text/prs.lines.tag', 190 | 'dssc': 'application/dssc+der', 191 | 'dtb': 'application/x-dtbook+xml', 192 | 'dtd': 'application/xml-dtd', 193 | 'dts': 'audio/vnd.dts', 194 | 'dtshd': 'audio/vnd.dts.hd', 195 | 'dump': 'application/octet-stream', 196 | 'dvb': 'video/vnd.dvb.file', 197 | 'dvi': 'application/x-dvi', 198 | 'dwf': 'model/vnd.dwf', 199 | 'dwg': 'image/vnd.dwg', 200 | 'dxf': 'image/vnd.dxf', 201 | 'dxp': 'application/vnd.spotfire.dxp', 202 | 'dxr': 'application/x-director', 203 | 'ecelp4800': 'audio/vnd.nuera.ecelp4800', 204 | 'ecelp7470': 'audio/vnd.nuera.ecelp7470', 205 | 'ecelp9600': 'audio/vnd.nuera.ecelp9600', 206 | 'ecma': 'application/ecmascript', 207 | 'edm': 'application/vnd.novadigm.edm', 208 | 'edx': 'application/vnd.novadigm.edx', 209 | 'efif': 'application/vnd.picsel', 210 | 'ei6': 'application/vnd.pg.osasli', 211 | 'elc': 'application/octet-stream', 212 | 'emf': 'application/x-msmetafile', 213 | 'eml': 'message/rfc822', 214 | 'emma': 'application/emma+xml', 215 | 'emz': 'application/x-msmetafile', 216 | 'eol': 'audio/vnd.digital-winds', 217 | 'eot': 'application/vnd.ms-fontobject', 218 | 'eps': 'application/postscript', 219 | 'epub': 'application/epub+zip', 220 | 'es3': 'application/vnd.eszigno3+xml', 221 | 'esa': 'application/vnd.osgi.subsystem', 222 | 'esf': 'application/vnd.epson.esf', 223 | 'et3': 'application/vnd.eszigno3+xml', 224 | 'etx': 'text/x-setext', 225 | 'eva': 'application/x-eva', 226 | 'evy': 'application/x-envoy', 227 | 'exe': 'application/x-msdownload', 228 | 'exi': 'application/exi', 229 | 'ext': 'application/vnd.novadigm.ext', 230 | 'ez': 'application/andrew-inset', 231 | 'ez2': 'application/vnd.ezpix-album', 232 | 'ez3': 'application/vnd.ezpix-package', 233 | 'f': 'text/x-fortran', 234 | 'f4v': 'video/x-f4v', 235 | 'f77': 'text/x-fortran', 236 | 'f90': 'text/x-fortran', 237 | 'fbs': 'image/vnd.fastbidsheet', 238 | 'fcdt': 'application/vnd.adobe.formscentral.fcdt', 239 | 'fcs': 'application/vnd.isac.fcs', 240 | 'fdf': 'application/vnd.fdf', 241 | 'fe_launch': 'application/vnd.denovo.fcselayout-link', 242 | 'fg5': 'application/vnd.fujitsu.oasysgp', 243 | 'fgd': 'application/x-director', 244 | 'fh': 'image/x-freehand', 245 | 'fh4': 'image/x-freehand', 246 | 'fh5': 'image/x-freehand', 247 | 'fh7': 'image/x-freehand', 248 | 'fhc': 'image/x-freehand', 249 | 'fig': 'application/x-xfig', 250 | 'flac': 'audio/x-flac', 251 | 'fli': 'video/x-fli', 252 | 'flo': 'application/vnd.micrografx.flo', 253 | 'flv': 'video/x-flv', 254 | 'flw': 'application/vnd.kde.kivio', 255 | 'flx': 'text/vnd.fmi.flexstor', 256 | 'fly': 'text/vnd.fly', 257 | 'fm': 'application/vnd.framemaker', 258 | 'fnc': 'application/vnd.frogans.fnc', 259 | 'for': 'text/x-fortran', 260 | 'fpx': 'image/vnd.fpx', 261 | 'frame': 'application/vnd.framemaker', 262 | 'fsc': 'application/vnd.fsc.weblaunch', 263 | 'fst': 'image/vnd.fst', 264 | 'ftc': 'application/vnd.fluxtime.clip', 265 | 'fti': 'application/vnd.anser-web-funds-transfer-initiation', 266 | 'fvt': 'video/vnd.fvt', 267 | 'fxp': 'application/vnd.adobe.fxp', 268 | 'fxpl': 'application/vnd.adobe.fxp', 269 | 'fzs': 'application/vnd.fuzzysheet', 270 | 'g2w': 'application/vnd.geoplan', 271 | 'g3': 'image/g3fax', 272 | 'g3w': 'application/vnd.geospace', 273 | 'gac': 'application/vnd.groove-account', 274 | 'gam': 'application/x-tads', 275 | 'gbr': 'application/rpki-ghostbusters', 276 | 'gca': 'application/x-gca-compressed', 277 | 'gdl': 'model/vnd.gdl', 278 | 'geo': 'application/vnd.dynageo', 279 | 'gex': 'application/vnd.geometry-explorer', 280 | 'ggb': 'application/vnd.geogebra.file', 281 | 'ggt': 'application/vnd.geogebra.tool', 282 | 'ghf': 'application/vnd.groove-help', 283 | 'gif': 'image/gif', 284 | 'gim': 'application/vnd.groove-identity-message', 285 | 'glb': 'model/gltf-binary', 286 | 'gltf': 'model/gltf+json', 287 | 'gml': 'application/gml+xml', 288 | 'gmx': 'application/vnd.gmx', 289 | 'gnumeric': 'application/x-gnumeric', 290 | 'gph': 'application/vnd.flographit', 291 | 'gpx': 'application/gpx+xml', 292 | 'gqf': 'application/vnd.grafeq', 293 | 'gqs': 'application/vnd.grafeq', 294 | 'gram': 'application/srgs', 295 | 'gramps': 'application/x-gramps-xml', 296 | 'gre': 'application/vnd.geometry-explorer', 297 | 'grv': 'application/vnd.groove-injector', 298 | 'grxml': 'application/srgs+xml', 299 | 'gsf': 'application/x-font-ghostscript', 300 | 'gtar': 'application/x-gtar', 301 | 'gtm': 'application/vnd.groove-tool-message', 302 | 'gtw': 'model/vnd.gtw', 303 | 'gv': 'text/vnd.graphviz', 304 | 'gxf': 'application/gxf', 305 | 'gxt': 'application/vnd.geonext', 306 | 'h': 'text/x-c', 307 | 'h261': 'video/h261', 308 | 'h263': 'video/h263', 309 | 'h264': 'video/h264', 310 | 'hal': 'application/vnd.hal+xml', 311 | 'hbci': 'application/vnd.hbci', 312 | 'hdf': 'application/x-hdf', 313 | 'heic': 'image/heic', 314 | 'heif': 'image/heif', 315 | 'hh': 'text/x-c', 316 | 'hlp': 'application/winhlp', 317 | 'hpgl': 'application/vnd.hp-hpgl', 318 | 'hpid': 'application/vnd.hp-hpid', 319 | 'hps': 'application/vnd.hp-hps', 320 | 'hqx': 'application/mac-binhex40', 321 | 'htke': 'application/vnd.kenameaapp', 322 | 'htm': 'text/html', 323 | 'html': 'text/html', 324 | 'hvd': 'application/vnd.yamaha.hv-dic', 325 | 'hvp': 'application/vnd.yamaha.hv-voice', 326 | 'hvs': 'application/vnd.yamaha.hv-script', 327 | 'i2g': 'application/vnd.intergeo', 328 | 'icc': 'application/vnd.iccprofile', 329 | 'ice': 'x-conference/x-cooltalk', 330 | 'icm': 'application/vnd.iccprofile', 331 | 'ico': 'image/x-icon', 332 | 'ics': 'text/calendar', 333 | 'ief': 'image/ief', 334 | 'ifb': 'text/calendar', 335 | 'ifm': 'application/vnd.shana.informed.formdata', 336 | 'iges': 'model/iges', 337 | 'igl': 'application/vnd.igloader', 338 | 'igm': 'application/vnd.insors.igm', 339 | 'igs': 'model/iges', 340 | 'igx': 'application/vnd.micrografx.igx', 341 | 'iif': 'application/vnd.shana.informed.interchange', 342 | 'imp': 'application/vnd.accpac.simply.imp', 343 | 'ims': 'application/vnd.ms-ims', 344 | 'in': 'text/plain', 345 | 'ink': 'application/inkml+xml', 346 | 'inkml': 'application/inkml+xml', 347 | 'install': 'application/x-install-instructions', 348 | 'iota': 'application/vnd.astraea-software.iota', 349 | 'ipfix': 'application/ipfix', 350 | 'ipk': 'application/vnd.shana.informed.package', 351 | 'irm': 'application/vnd.ibm.rights-management', 352 | 'irp': 'application/vnd.irepository.package+xml', 353 | 'iso': 'application/x-iso9660-image', 354 | 'itp': 'application/vnd.shana.informed.formtemplate', 355 | 'ivp': 'application/vnd.immervision-ivp', 356 | 'ivu': 'application/vnd.immervision-ivu', 357 | 'jad': 'text/vnd.sun.j2me.app-descriptor', 358 | 'jam': 'application/vnd.jam', 359 | 'jar': 'application/java-archive', 360 | 'java': 'text/x-java-source', 361 | 'jisp': 'application/vnd.jisp', 362 | 'jlt': 'application/vnd.hp-jlyt', 363 | 'jnlp': 'application/x-java-jnlp-file', 364 | 'joda': 'application/vnd.joost.joda-archive', 365 | 'jpe': 'image/jpeg', 366 | 'jpeg': 'image/jpeg', 367 | 'jpg': 'image/jpeg', 368 | 'jpgm': 'video/jpm', 369 | 'jpgv': 'video/jpeg', 370 | 'jpm': 'video/jpm', 371 | 'js': 'text/javascript', 372 | 'json': 'application/json', 373 | 'jsonml': 'application/jsonml+json', 374 | 'kar': 'audio/midi', 375 | 'karbon': 'application/vnd.kde.karbon', 376 | 'kfo': 'application/vnd.kde.kformula', 377 | 'kia': 'application/vnd.kidspiration', 378 | 'kml': 'application/vnd.google-earth.kml+xml', 379 | 'kmz': 'application/vnd.google-earth.kmz', 380 | 'kne': 'application/vnd.kinar', 381 | 'knp': 'application/vnd.kinar', 382 | 'kon': 'application/vnd.kde.kontour', 383 | 'kpr': 'application/vnd.kde.kpresenter', 384 | 'kpt': 'application/vnd.kde.kpresenter', 385 | 'kpxx': 'application/vnd.ds-keypoint', 386 | 'ksp': 'application/vnd.kde.kspread', 387 | 'ktr': 'application/vnd.kahootz', 388 | 'ktx': 'image/ktx', 389 | 'ktz': 'application/vnd.kahootz', 390 | 'kwd': 'application/vnd.kde.kword', 391 | 'kwt': 'application/vnd.kde.kword', 392 | 'lasxml': 'application/vnd.las.las+xml', 393 | 'latex': 'application/x-latex', 394 | 'lbd': 'application/vnd.llamagraphics.life-balance.desktop', 395 | 'lbe': 'application/vnd.llamagraphics.life-balance.exchange+xml', 396 | 'les': 'application/vnd.hhe.lesson-player', 397 | 'lha': 'application/x-lzh-compressed', 398 | 'link66': 'application/vnd.route66.link66+xml', 399 | 'list': 'text/plain', 400 | 'list3820': 'application/vnd.ibm.modcap', 401 | 'listafp': 'application/vnd.ibm.modcap', 402 | 'lnk': 'application/x-ms-shortcut', 403 | 'log': 'text/plain', 404 | 'lostxml': 'application/lost+xml', 405 | 'lrf': 'application/octet-stream', 406 | 'lrm': 'application/vnd.ms-lrm', 407 | 'ltf': 'application/vnd.frogans.ltf', 408 | 'lvp': 'audio/vnd.lucent.voice', 409 | 'lwp': 'application/vnd.lotus-wordpro', 410 | 'lzh': 'application/x-lzh-compressed', 411 | 'm13': 'application/x-msmediaview', 412 | 'm14': 'application/x-msmediaview', 413 | 'm1v': 'video/mpeg', 414 | 'm21': 'application/mp21', 415 | 'm2a': 'audio/mpeg', 416 | 'm2v': 'video/mpeg', 417 | 'm3a': 'audio/mpeg', 418 | 'm3u': 'audio/x-mpegurl', 419 | 'm3u8': 'application/vnd.apple.mpegurl', 420 | // Source: https://datatracker.ietf.org/doc/html/rfc4337#section-2 421 | 'm4a': 'audio/mp4', 422 | 'm4b': 'audio/mp4', 423 | 'm4u': 'video/vnd.mpegurl', 424 | 'm4v': 'video/x-m4v', 425 | 'ma': 'application/mathematica', 426 | 'mads': 'application/mads+xml', 427 | 'mag': 'application/vnd.ecowin.chart', 428 | 'maker': 'application/vnd.framemaker', 429 | 'man': 'text/troff', 430 | 'mar': 'application/octet-stream', 431 | 'mathml': 'application/mathml+xml', 432 | 'mb': 'application/mathematica', 433 | 'mbk': 'application/vnd.mobius.mbk', 434 | 'mbox': 'application/mbox', 435 | 'mc1': 'application/vnd.medcalcdata', 436 | 'mcd': 'application/vnd.mcd', 437 | 'mcurl': 'text/vnd.curl.mcurl', 438 | // https://www.rfc-editor.org/rfc/rfc7763 439 | 'md': 'text/markdown', 440 | 'markdown': 'text/markdown', 441 | 'mdb': 'application/x-msaccess', 442 | 'mdi': 'image/vnd.ms-modi', 443 | 'me': 'text/troff', 444 | 'mesh': 'model/mesh', 445 | 'meta4': 'application/metalink4+xml', 446 | 'metalink': 'application/metalink+xml', 447 | 'mets': 'application/mets+xml', 448 | 'mfm': 'application/vnd.mfmp', 449 | 'mft': 'application/rpki-manifest', 450 | 'mgp': 'application/vnd.osgeo.mapguide.package', 451 | 'mgz': 'application/vnd.proteus.magazine', 452 | 'mid': 'audio/midi', 453 | 'midi': 'audio/midi', 454 | 'mie': 'application/x-mie', 455 | 'mif': 'application/vnd.mif', 456 | 'mime': 'message/rfc822', 457 | 'mj2': 'video/mj2', 458 | 'mjp2': 'video/mj2', 459 | 'mjs': 'text/javascript', 460 | 'mk3d': 'video/x-matroska', 461 | 'mka': 'audio/x-matroska', 462 | 'mks': 'video/x-matroska', 463 | 'mkv': 'video/x-matroska', 464 | 'mlp': 'application/vnd.dolby.mlp', 465 | 'mmd': 'application/vnd.chipnuts.karaoke-mmd', 466 | 'mmf': 'application/vnd.smaf', 467 | 'mmr': 'image/vnd.fujixerox.edmics-mmr', 468 | 'mng': 'video/x-mng', 469 | 'mny': 'application/x-msmoney', 470 | 'mobi': 'application/x-mobipocket-ebook', 471 | 'mods': 'application/mods+xml', 472 | 'mov': 'video/quicktime', 473 | 'movie': 'video/x-sgi-movie', 474 | 'mp2': 'audio/mpeg', 475 | 'mp21': 'application/mp21', 476 | 'mp2a': 'audio/mpeg', 477 | 'mp3': 'audio/mpeg', 478 | 'mp4': 'video/mp4', 479 | 'mp4a': 'audio/mp4', 480 | 'mp4s': 'application/mp4', 481 | 'mp4v': 'video/mp4', 482 | 'mpc': 'application/vnd.mophun.certificate', 483 | 'mpe': 'video/mpeg', 484 | 'mpeg': 'video/mpeg', 485 | 'mpg': 'video/mpeg', 486 | 'mpg4': 'video/mp4', 487 | 'mpga': 'audio/mpeg', 488 | 'mpkg': 'application/vnd.apple.installer+xml', 489 | 'mpm': 'application/vnd.blueice.multipass', 490 | 'mpn': 'application/vnd.mophun.application', 491 | 'mpp': 'application/vnd.ms-project', 492 | 'mpt': 'application/vnd.ms-project', 493 | 'mpy': 'application/vnd.ibm.minipay', 494 | 'mqy': 'application/vnd.mobius.mqy', 495 | 'mrc': 'application/marc', 496 | 'mrcx': 'application/marcxml+xml', 497 | 'ms': 'text/troff', 498 | 'mscml': 'application/mediaservercontrol+xml', 499 | 'mseed': 'application/vnd.fdsn.mseed', 500 | 'mseq': 'application/vnd.mseq', 501 | 'msf': 'application/vnd.epson.msf', 502 | 'msh': 'model/mesh', 503 | 'msi': 'application/x-msdownload', 504 | 'msl': 'application/vnd.mobius.msl', 505 | 'msty': 'application/vnd.muvee.style', 506 | 'mts': 'model/vnd.mts', 507 | 'mus': 'application/vnd.musician', 508 | 'musicxml': 'application/vnd.recordare.musicxml+xml', 509 | 'mvb': 'application/x-msmediaview', 510 | 'mwf': 'application/vnd.mfer', 511 | 'mxf': 'application/mxf', 512 | 'mxl': 'application/vnd.recordare.musicxml', 513 | 'mxml': 'application/xv+xml', 514 | 'mxs': 'application/vnd.triscape.mxs', 515 | 'mxu': 'video/vnd.mpegurl', 516 | 'n-gage': 'application/vnd.nokia.n-gage.symbian.install', 517 | 'n3': 'text/n3', 518 | 'nb': 'application/mathematica', 519 | 'nbp': 'application/vnd.wolfram.player', 520 | 'nc': 'application/x-netcdf', 521 | 'ncx': 'application/x-dtbncx+xml', 522 | 'nfo': 'text/x-nfo', 523 | 'ngdat': 'application/vnd.nokia.n-gage.data', 524 | 'nitf': 'application/vnd.nitf', 525 | 'nlu': 'application/vnd.neurolanguage.nlu', 526 | 'nml': 'application/vnd.enliven', 527 | 'nnd': 'application/vnd.noblenet-directory', 528 | 'nns': 'application/vnd.noblenet-sealer', 529 | 'nnw': 'application/vnd.noblenet-web', 530 | 'npx': 'image/vnd.net-fpx', 531 | 'nsc': 'application/x-conference', 532 | 'nsf': 'application/vnd.lotus-notes', 533 | 'ntf': 'application/vnd.nitf', 534 | 'nzb': 'application/x-nzb', 535 | 'oa2': 'application/vnd.fujitsu.oasys2', 536 | 'oa3': 'application/vnd.fujitsu.oasys3', 537 | 'oas': 'application/vnd.fujitsu.oasys', 538 | 'obd': 'application/x-msbinder', 539 | 'obj': 'application/x-tgif', 540 | 'oda': 'application/oda', 541 | 'odb': 'application/vnd.oasis.opendocument.database', 542 | 'odc': 'application/vnd.oasis.opendocument.chart', 543 | 'odf': 'application/vnd.oasis.opendocument.formula', 544 | 'odft': 'application/vnd.oasis.opendocument.formula-template', 545 | 'odg': 'application/vnd.oasis.opendocument.graphics', 546 | 'odi': 'application/vnd.oasis.opendocument.image', 547 | 'odm': 'application/vnd.oasis.opendocument.text-master', 548 | 'odp': 'application/vnd.oasis.opendocument.presentation', 549 | 'ods': 'application/vnd.oasis.opendocument.spreadsheet', 550 | 'odt': 'application/vnd.oasis.opendocument.text', 551 | 'oga': 'audio/ogg', 552 | 'ogg': 'audio/ogg', 553 | 'ogv': 'video/ogg', 554 | 'ogx': 'application/ogg', 555 | 'omdoc': 'application/omdoc+xml', 556 | 'onepkg': 'application/onenote', 557 | 'onetmp': 'application/onenote', 558 | 'onetoc': 'application/onenote', 559 | 'onetoc2': 'application/onenote', 560 | 'opf': 'application/oebps-package+xml', 561 | 'opml': 'text/x-opml', 562 | 'oprc': 'application/vnd.palm', 563 | 'org': 'application/vnd.lotus-organizer', 564 | 'osf': 'application/vnd.yamaha.openscoreformat', 565 | 'osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 566 | 'otc': 'application/vnd.oasis.opendocument.chart-template', 567 | 'otf': 'application/x-font-otf', 568 | 'otg': 'application/vnd.oasis.opendocument.graphics-template', 569 | 'oth': 'application/vnd.oasis.opendocument.text-web', 570 | 'oti': 'application/vnd.oasis.opendocument.image-template', 571 | 'otp': 'application/vnd.oasis.opendocument.presentation-template', 572 | 'ots': 'application/vnd.oasis.opendocument.spreadsheet-template', 573 | 'ott': 'application/vnd.oasis.opendocument.text-template', 574 | 'oxps': 'application/oxps', 575 | 'oxt': 'application/vnd.openofficeorg.extension', 576 | 'p': 'text/x-pascal', 577 | 'p10': 'application/pkcs10', 578 | 'p12': 'application/x-pkcs12', 579 | 'p7b': 'application/x-pkcs7-certificates', 580 | 'p7c': 'application/pkcs7-mime', 581 | 'p7m': 'application/pkcs7-mime', 582 | 'p7r': 'application/x-pkcs7-certreqresp', 583 | 'p7s': 'application/pkcs7-signature', 584 | 'p8': 'application/pkcs8', 585 | 'pas': 'text/x-pascal', 586 | 'paw': 'application/vnd.pawaafile', 587 | 'pbd': 'application/vnd.powerbuilder6', 588 | 'pbm': 'image/x-portable-bitmap', 589 | 'pcap': 'application/vnd.tcpdump.pcap', 590 | 'pcf': 'application/x-font-pcf', 591 | 'pcl': 'application/vnd.hp-pcl', 592 | 'pclxl': 'application/vnd.hp-pclxl', 593 | 'pct': 'image/x-pict', 594 | 'pcurl': 'application/vnd.curl.pcurl', 595 | 'pcx': 'image/x-pcx', 596 | 'pdb': 'application/vnd.palm', 597 | 'pdf': 'application/pdf', 598 | 'pfa': 'application/x-font-type1', 599 | 'pfb': 'application/x-font-type1', 600 | 'pfm': 'application/x-font-type1', 601 | 'pfr': 'application/font-tdpfr', 602 | 'pfx': 'application/x-pkcs12', 603 | 'pgm': 'image/x-portable-graymap', 604 | 'pgn': 'application/x-chess-pgn', 605 | 'pgp': 'application/pgp-encrypted', 606 | 'pic': 'image/x-pict', 607 | 'pkg': 'application/octet-stream', 608 | 'pki': 'application/pkixcmp', 609 | 'pkipath': 'application/pkix-pkipath', 610 | 'plb': 'application/vnd.3gpp.pic-bw-large', 611 | 'plc': 'application/vnd.mobius.plc', 612 | 'plf': 'application/vnd.pocketlearn', 613 | 'pls': 'application/pls+xml', 614 | 'pml': 'application/vnd.ctc-posml', 615 | 'png': 'image/png', 616 | 'pnm': 'image/x-portable-anymap', 617 | 'portpkg': 'application/vnd.macports.portpkg', 618 | 'pot': 'application/vnd.ms-powerpoint', 619 | 'potm': 'application/vnd.ms-powerpoint.template.macroenabled.12', 620 | 'potx': 621 | 'application/vnd.openxmlformats-officedocument.presentationml.template', 622 | 'ppam': 'application/vnd.ms-powerpoint.addin.macroenabled.12', 623 | 'ppd': 'application/vnd.cups-ppd', 624 | 'ppm': 'image/x-portable-pixmap', 625 | 'pps': 'application/vnd.ms-powerpoint', 626 | 'ppsm': 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', 627 | 'ppsx': 628 | 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 629 | 'ppt': 'application/vnd.ms-powerpoint', 630 | 'pptm': 'application/vnd.ms-powerpoint.presentation.macroenabled.12', 631 | 'pptx': 632 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 633 | 'pqa': 'application/vnd.palm', 634 | 'prc': 'application/x-mobipocket-ebook', 635 | 'pre': 'application/vnd.lotus-freelance', 636 | 'prf': 'application/pics-rules', 637 | 'ps': 'application/postscript', 638 | 'psb': 'application/vnd.3gpp.pic-bw-small', 639 | 'psd': 'image/vnd.adobe.photoshop', 640 | 'psf': 'application/x-font-linux-psf', 641 | 'pskcxml': 'application/pskc+xml', 642 | 'ptid': 'application/vnd.pvi.ptid1', 643 | 'pub': 'application/x-mspublisher', 644 | 'pvb': 'application/vnd.3gpp.pic-bw-var', 645 | 'pwn': 'application/vnd.3m.post-it-notes', 646 | 'pya': 'audio/vnd.ms-playready.media.pya', 647 | 'pyv': 'video/vnd.ms-playready.media.pyv', 648 | 'qam': 'application/vnd.epson.quickanime', 649 | 'qbo': 'application/vnd.intu.qbo', 650 | 'qfx': 'application/vnd.intu.qfx', 651 | 'qps': 'application/vnd.publishare-delta-tree', 652 | 'qt': 'video/quicktime', 653 | 'qwd': 'application/vnd.quark.quarkxpress', 654 | 'qwt': 'application/vnd.quark.quarkxpress', 655 | 'qxb': 'application/vnd.quark.quarkxpress', 656 | 'qxd': 'application/vnd.quark.quarkxpress', 657 | 'qxl': 'application/vnd.quark.quarkxpress', 658 | 'qxt': 'application/vnd.quark.quarkxpress', 659 | 'ra': 'audio/x-pn-realaudio', 660 | 'ram': 'audio/x-pn-realaudio', 661 | 'rar': 'application/x-rar-compressed', 662 | 'ras': 'image/x-cmu-raster', 663 | 'rcprofile': 'application/vnd.ipunplugged.rcprofile', 664 | 'rdf': 'application/rdf+xml', 665 | 'rdz': 'application/vnd.data-vision.rdz', 666 | 'rep': 'application/vnd.businessobjects', 667 | 'res': 'application/x-dtbresource+xml', 668 | 'rgb': 'image/x-rgb', 669 | 'rif': 'application/reginfo+xml', 670 | 'rip': 'audio/vnd.rip', 671 | 'ris': 'application/x-research-info-systems', 672 | 'rl': 'application/resource-lists+xml', 673 | 'rlc': 'image/vnd.fujixerox.edmics-rlc', 674 | 'rld': 'application/resource-lists-diff+xml', 675 | 'rm': 'application/vnd.rn-realmedia', 676 | 'rmi': 'audio/midi', 677 | 'rmp': 'audio/x-pn-realaudio-plugin', 678 | 'rms': 'application/vnd.jcp.javame.midlet-rms', 679 | 'rmvb': 'application/vnd.rn-realmedia-vbr', 680 | 'rnc': 'application/relax-ng-compact-syntax', 681 | 'roa': 'application/rpki-roa', 682 | 'roff': 'text/troff', 683 | 'rp9': 'application/vnd.cloanto.rp9', 684 | 'rpss': 'application/vnd.nokia.radio-presets', 685 | 'rpst': 'application/vnd.nokia.radio-preset', 686 | 'rq': 'application/sparql-query', 687 | 'rs': 'application/rls-services+xml', 688 | 'rsd': 'application/rsd+xml', 689 | 'rss': 'application/rss+xml', 690 | 'rtf': 'application/rtf', 691 | 'rtx': 'text/richtext', 692 | 's': 'text/x-asm', 693 | 's3m': 'audio/s3m', 694 | 'saf': 'application/vnd.yamaha.smaf-audio', 695 | 'sbml': 'application/sbml+xml', 696 | 'sc': 'application/vnd.ibm.secure-container', 697 | 'scd': 'application/x-msschedule', 698 | 'scm': 'application/vnd.lotus-screencam', 699 | 'scq': 'application/scvp-cv-request', 700 | 'scs': 'application/scvp-cv-response', 701 | 'scurl': 'text/vnd.curl.scurl', 702 | 'sda': 'application/vnd.stardivision.draw', 703 | 'sdc': 'application/vnd.stardivision.calc', 704 | 'sdd': 'application/vnd.stardivision.impress', 705 | 'sdkd': 'application/vnd.solent.sdkm+xml', 706 | 'sdkm': 'application/vnd.solent.sdkm+xml', 707 | 'sdp': 'application/sdp', 708 | 'sdw': 'application/vnd.stardivision.writer', 709 | 'see': 'application/vnd.seemail', 710 | 'seed': 'application/vnd.fdsn.seed', 711 | 'sema': 'application/vnd.sema', 712 | 'semd': 'application/vnd.semd', 713 | 'semf': 'application/vnd.semf', 714 | 'ser': 'application/java-serialized-object', 715 | 'setpay': 'application/set-payment-initiation', 716 | 'setreg': 'application/set-registration-initiation', 717 | 'sfd-hdstx': 'application/vnd.hydrostatix.sof-data', 718 | 'sfs': 'application/vnd.spotfire.sfs', 719 | 'sfv': 'text/x-sfv', 720 | 'sgi': 'image/sgi', 721 | 'sgl': 'application/vnd.stardivision.writer-global', 722 | 'sgm': 'text/sgml', 723 | 'sgml': 'text/sgml', 724 | 'sh': 'application/x-sh', 725 | 'shar': 'application/x-shar', 726 | 'shf': 'application/shf+xml', 727 | 'sid': 'image/x-mrsid-image', 728 | 'sig': 'application/pgp-signature', 729 | 'sil': 'audio/silk', 730 | 'silo': 'model/mesh', 731 | 'sis': 'application/vnd.symbian.install', 732 | 'sisx': 'application/vnd.symbian.install', 733 | 'sit': 'application/x-stuffit', 734 | 'sitx': 'application/x-stuffitx', 735 | 'skd': 'application/vnd.koan', 736 | 'skm': 'application/vnd.koan', 737 | 'skp': 'application/vnd.koan', 738 | 'skt': 'application/vnd.koan', 739 | 'sldm': 'application/vnd.ms-powerpoint.slide.macroenabled.12', 740 | 'sldx': 'application/vnd.openxmlformats-officedocument.presentationml.slide', 741 | 'slt': 'application/vnd.epson.salt', 742 | 'sm': 'application/vnd.stepmania.stepchart', 743 | 'smf': 'application/vnd.stardivision.math', 744 | 'smi': 'application/smil+xml', 745 | 'smil': 'application/smil+xml', 746 | 'smv': 'video/x-smv', 747 | 'smzip': 'application/vnd.stepmania.package', 748 | 'snd': 'audio/basic', 749 | 'snf': 'application/x-font-snf', 750 | 'so': 'application/octet-stream', 751 | 'spc': 'application/x-pkcs7-certificates', 752 | 'spf': 'application/vnd.yamaha.smaf-phrase', 753 | 'spl': 'application/x-futuresplash', 754 | 'spot': 'text/vnd.in3d.spot', 755 | 'spp': 'application/scvp-vp-response', 756 | 'spq': 'application/scvp-vp-request', 757 | 'spx': 'audio/ogg', 758 | 'sql': 'application/x-sql', 759 | 'src': 'application/x-wais-source', 760 | 'srt': 'application/x-subrip', 761 | 'sru': 'application/sru+xml', 762 | 'srx': 'application/sparql-results+xml', 763 | 'ssdl': 'application/ssdl+xml', 764 | 'sse': 'application/vnd.kodak-descriptor', 765 | 'ssf': 'application/vnd.epson.ssf', 766 | 'ssml': 'application/ssml+xml', 767 | 'st': 'application/vnd.sailingtracker.track', 768 | 'stc': 'application/vnd.sun.xml.calc.template', 769 | 'std': 'application/vnd.sun.xml.draw.template', 770 | 'stf': 'application/vnd.wt.stf', 771 | 'sti': 'application/vnd.sun.xml.impress.template', 772 | 'stk': 'application/hyperstudio', 773 | 'stl': 'application/vnd.ms-pki.stl', 774 | 'str': 'application/vnd.pg.format', 775 | 'stw': 'application/vnd.sun.xml.writer.template', 776 | 'sub': 'text/vnd.dvb.subtitle', 777 | 'sus': 'application/vnd.sus-calendar', 778 | 'susp': 'application/vnd.sus-calendar', 779 | 'sv4cpio': 'application/x-sv4cpio', 780 | 'sv4crc': 'application/x-sv4crc', 781 | 'svc': 'application/vnd.dvb.service', 782 | 'svd': 'application/vnd.svd', 783 | 'svg': 'image/svg+xml', 784 | 'svgz': 'image/svg+xml', 785 | 'swa': 'application/x-director', 786 | 'swf': 'application/x-shockwave-flash', 787 | 'swi': 'application/vnd.aristanetworks.swi', 788 | 'sxc': 'application/vnd.sun.xml.calc', 789 | 'sxd': 'application/vnd.sun.xml.draw', 790 | 'sxg': 'application/vnd.sun.xml.writer.global', 791 | 'sxi': 'application/vnd.sun.xml.impress', 792 | 'sxm': 'application/vnd.sun.xml.math', 793 | 'sxw': 'application/vnd.sun.xml.writer', 794 | 't': 'text/troff', 795 | 't3': 'application/x-t3vm-image', 796 | 'taglet': 'application/vnd.mynfc', 797 | 'tao': 'application/vnd.tao.intent-module-archive', 798 | 'tar': 'application/x-tar', 799 | 'tcap': 'application/vnd.3gpp2.tcap', 800 | 'tcl': 'application/x-tcl', 801 | 'teacher': 'application/vnd.smart.teacher', 802 | 'tei': 'application/tei+xml', 803 | 'teicorpus': 'application/tei+xml', 804 | 'tex': 'application/x-tex', 805 | 'texi': 'application/x-texinfo', 806 | 'texinfo': 'application/x-texinfo', 807 | 'text': 'text/plain', 808 | 'tfi': 'application/thraud+xml', 809 | 'tfm': 'application/x-tex-tfm', 810 | 'tga': 'image/x-tga', 811 | 'thmx': 'application/vnd.ms-officetheme', 812 | 'tif': 'image/tiff', 813 | 'tiff': 'image/tiff', 814 | 'tmo': 'application/vnd.tmobile-livetv', 815 | // Source: https://toml.io/en/v1.0.0#mime-type 816 | 'toml': 'application/toml', 817 | 'torrent': 'application/x-bittorrent', 818 | 'tpl': 'application/vnd.groove-tool-template', 819 | 'tpt': 'application/vnd.trid.tpt', 820 | 'tr': 'text/troff', 821 | 'tra': 'application/vnd.trueapp', 822 | 'trm': 'application/x-msterminal', 823 | 'tsd': 'application/timestamped-data', 824 | 'tsv': 'text/tab-separated-values', 825 | 'ttc': 'application/x-font-ttf', 826 | 'ttf': 'application/x-font-ttf', 827 | 'ttl': 'text/turtle', 828 | 'twd': 'application/vnd.simtech-mindmapper', 829 | 'twds': 'application/vnd.simtech-mindmapper', 830 | 'txd': 'application/vnd.genomatix.tuxedo', 831 | 'txf': 'application/vnd.mobius.txf', 832 | 'txt': 'text/plain', 833 | 'u32': 'application/x-authorware-bin', 834 | 'udeb': 'application/x-debian-package', 835 | 'ufd': 'application/vnd.ufdl', 836 | 'ufdl': 'application/vnd.ufdl', 837 | 'ulx': 'application/x-glulx', 838 | 'umj': 'application/vnd.umajin', 839 | 'unityweb': 'application/vnd.unity', 840 | 'uoml': 'application/vnd.uoml+xml', 841 | 'uri': 'text/uri-list', 842 | 'uris': 'text/uri-list', 843 | 'urls': 'text/uri-list', 844 | 'ustar': 'application/x-ustar', 845 | 'utz': 'application/vnd.uiq.theme', 846 | 'uu': 'text/x-uuencode', 847 | 'uva': 'audio/vnd.dece.audio', 848 | 'uvd': 'application/vnd.dece.data', 849 | 'uvf': 'application/vnd.dece.data', 850 | 'uvg': 'image/vnd.dece.graphic', 851 | 'uvh': 'video/vnd.dece.hd', 852 | 'uvi': 'image/vnd.dece.graphic', 853 | 'uvm': 'video/vnd.dece.mobile', 854 | 'uvp': 'video/vnd.dece.pd', 855 | 'uvs': 'video/vnd.dece.sd', 856 | 'uvt': 'application/vnd.dece.ttml+xml', 857 | 'uvu': 'video/vnd.uvvu.mp4', 858 | 'uvv': 'video/vnd.dece.video', 859 | 'uvva': 'audio/vnd.dece.audio', 860 | 'uvvd': 'application/vnd.dece.data', 861 | 'uvvf': 'application/vnd.dece.data', 862 | 'uvvg': 'image/vnd.dece.graphic', 863 | 'uvvh': 'video/vnd.dece.hd', 864 | 'uvvi': 'image/vnd.dece.graphic', 865 | 'uvvm': 'video/vnd.dece.mobile', 866 | 'uvvp': 'video/vnd.dece.pd', 867 | 'uvvs': 'video/vnd.dece.sd', 868 | 'uvvt': 'application/vnd.dece.ttml+xml', 869 | 'uvvu': 'video/vnd.uvvu.mp4', 870 | 'uvvv': 'video/vnd.dece.video', 871 | 'uvvx': 'application/vnd.dece.unspecified', 872 | 'uvvz': 'application/vnd.dece.zip', 873 | 'uvx': 'application/vnd.dece.unspecified', 874 | 'uvz': 'application/vnd.dece.zip', 875 | 'vcard': 'text/vcard', 876 | 'vcd': 'application/x-cdlink', 877 | 'vcf': 'text/x-vcard', 878 | 'vcg': 'application/vnd.groove-vcard', 879 | 'vcs': 'text/x-vcalendar', 880 | 'vcx': 'application/vnd.vcx', 881 | 'vis': 'application/vnd.visionary', 882 | 'viv': 'video/vnd.vivo', 883 | 'vob': 'video/x-ms-vob', 884 | 'vor': 'application/vnd.stardivision.writer', 885 | 'vox': 'application/x-authorware-bin', 886 | 'vrml': 'model/vrml', 887 | 'vsd': 'application/vnd.visio', 888 | 'vsf': 'application/vnd.vsf', 889 | 'vss': 'application/vnd.visio', 890 | 'vst': 'application/vnd.visio', 891 | 'vsw': 'application/vnd.visio', 892 | 'vtu': 'model/vnd.vtu', 893 | 'vxml': 'application/voicexml+xml', 894 | 'w3d': 'application/x-director', 895 | 'wad': 'application/x-doom', 896 | 'wasm': 'application/wasm', 897 | 'wav': 'audio/x-wav', 898 | 'wax': 'audio/x-ms-wax', 899 | 'wbmp': 'image/vnd.wap.wbmp', 900 | 'wbs': 'application/vnd.criticaltools.wbs+xml', 901 | 'wbxml': 'application/vnd.wap.wbxml', 902 | 'wcm': 'application/vnd.ms-works', 903 | 'wdb': 'application/vnd.ms-works', 904 | 'wdp': 'image/vnd.ms-photo', 905 | 'weba': 'audio/webm', 906 | 'webm': 'video/webm', 907 | // Source: https://w3c.github.io/manifest/#media-type-registration 908 | 'webmanifest': 'application/manifest+json', 909 | 'webp': 'image/webp', 910 | 'wg': 'application/vnd.pmi.widget', 911 | 'wgt': 'application/widget', 912 | 'wks': 'application/vnd.ms-works', 913 | 'wm': 'video/x-ms-wm', 914 | 'wma': 'audio/x-ms-wma', 915 | 'wmd': 'application/x-ms-wmd', 916 | 'wmf': 'application/x-msmetafile', 917 | 'wml': 'text/vnd.wap.wml', 918 | 'wmlc': 'application/vnd.wap.wmlc', 919 | 'wmls': 'text/vnd.wap.wmlscript', 920 | 'wmlsc': 'application/vnd.wap.wmlscriptc', 921 | 'wmv': 'video/x-ms-wmv', 922 | 'wmx': 'video/x-ms-wmx', 923 | 'wmz': 'application/x-ms-wmz', 924 | 'woff': 'application/x-font-woff', 925 | 'woff2': 'font/woff2', 926 | 'wpd': 'application/vnd.wordperfect', 927 | 'wpl': 'application/vnd.ms-wpl', 928 | 'wps': 'application/vnd.ms-works', 929 | 'wqd': 'application/vnd.wqd', 930 | 'wri': 'application/x-mswrite', 931 | 'wrl': 'model/vrml', 932 | 'wsdl': 'application/wsdl+xml', 933 | 'wspolicy': 'application/wspolicy+xml', 934 | 'wtb': 'application/vnd.webturbo', 935 | 'wvx': 'video/x-ms-wvx', 936 | 'x32': 'application/x-authorware-bin', 937 | 'x3d': 'model/x3d+xml', 938 | 'x3db': 'model/x3d+binary', 939 | 'x3dbz': 'model/x3d+binary', 940 | 'x3dv': 'model/x3d+vrml', 941 | 'x3dvz': 'model/x3d+vrml', 942 | 'x3dz': 'model/x3d+xml', 943 | 'xaml': 'application/xaml+xml', 944 | 'xap': 'application/x-silverlight-app', 945 | 'xar': 'application/vnd.xara', 946 | 'xbap': 'application/x-ms-xbap', 947 | 'xbd': 'application/vnd.fujixerox.docuworks.binder', 948 | 'xbm': 'image/x-xbitmap', 949 | 'xdf': 'application/xcap-diff+xml', 950 | 'xdm': 'application/vnd.syncml.dm+xml', 951 | 'xdp': 'application/vnd.adobe.xdp+xml', 952 | 'xdssc': 'application/dssc+xml', 953 | 'xdw': 'application/vnd.fujixerox.docuworks', 954 | 'xenc': 'application/xenc+xml', 955 | 'xer': 'application/patch-ops-error+xml', 956 | 'xfdf': 'application/vnd.adobe.xfdf', 957 | 'xfdl': 'application/vnd.xfdl', 958 | 'xht': 'application/xhtml+xml', 959 | 'xhtml': 'application/xhtml+xml', 960 | 'xhvml': 'application/xv+xml', 961 | 'xif': 'image/vnd.xiff', 962 | 'xla': 'application/vnd.ms-excel', 963 | 'xlam': 'application/vnd.ms-excel.addin.macroenabled.12', 964 | 'xlc': 'application/vnd.ms-excel', 965 | 'xlf': 'application/x-xliff+xml', 966 | 'xlm': 'application/vnd.ms-excel', 967 | 'xls': 'application/vnd.ms-excel', 968 | 'xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12', 969 | 'xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12', 970 | 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 971 | 'xlt': 'application/vnd.ms-excel', 972 | 'xltm': 'application/vnd.ms-excel.template.macroenabled.12', 973 | 'xltx': 974 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 975 | 'xlw': 'application/vnd.ms-excel', 976 | 'xm': 'audio/xm', 977 | 'xml': 'application/xml', 978 | 'xo': 'application/vnd.olpc-sugar', 979 | 'xop': 'application/xop+xml', 980 | 'xpi': 'application/x-xpinstall', 981 | 'xpl': 'application/xproc+xml', 982 | 'xpm': 'image/x-xpixmap', 983 | 'xpr': 'application/vnd.is-xpr', 984 | 'xps': 'application/vnd.ms-xpsdocument', 985 | 'xpw': 'application/vnd.intercon.formnet', 986 | 'xpx': 'application/vnd.intercon.formnet', 987 | 'xsl': 'application/xml', 988 | 'xslt': 'application/xslt+xml', 989 | 'xsm': 'application/vnd.syncml+xml', 990 | 'xspf': 'application/xspf+xml', 991 | 'xul': 'application/vnd.mozilla.xul+xml', 992 | 'xvm': 'application/xv+xml', 993 | 'xvml': 'application/xv+xml', 994 | 'xwd': 'image/x-xwindowdump', 995 | 'xyz': 'chemical/x-xyz', 996 | 'xz': 'application/x-xz', 997 | 'yang': 'application/yang', 998 | 'yin': 'application/yin+xml', 999 | 'z1': 'application/x-zmachine', 1000 | 'z2': 'application/x-zmachine', 1001 | 'z3': 'application/x-zmachine', 1002 | 'z4': 'application/x-zmachine', 1003 | 'z5': 'application/x-zmachine', 1004 | 'z6': 'application/x-zmachine', 1005 | 'z7': 'application/x-zmachine', 1006 | 'z8': 'application/x-zmachine', 1007 | 'zaz': 'application/vnd.zzazz.deck+xml', 1008 | 'zip': 'application/zip', 1009 | 'zir': 'application/vnd.zul', 1010 | 'zirz': 'application/vnd.zul', 1011 | 'zmm': 'application/vnd.handheld-entertainment+xml', 1012 | }; 1013 | -------------------------------------------------------------------------------- /lib/src/magic_number.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | class MagicNumber { 6 | final String mimeType; 7 | final List numbers; 8 | final List? mask; 9 | 10 | const MagicNumber(this.mimeType, this.numbers, {this.mask}); 11 | 12 | bool matches(List header) { 13 | if (header.length < numbers.length) return false; 14 | 15 | for (var i = 0; i < numbers.length; i++) { 16 | if (mask != null) { 17 | if ((mask![i] & numbers[i]) != (mask![i] & header[i])) return false; 18 | } else { 19 | if (numbers[i] != header[i]) return false; 20 | } 21 | } 22 | 23 | return true; 24 | } 25 | } 26 | 27 | const int initialMagicNumbersMaxLength = 12; 28 | 29 | const List initialMagicNumbers = [ 30 | MagicNumber('application/pdf', [0x25, 0x50, 0x44, 0x46]), 31 | MagicNumber('application/postscript', [0x25, 0x51]), 32 | 33 | /// AIFF is based on the EA IFF 85 Standard for Interchange Format Files. 34 | /// -> 4 bytes have the ASCII characters 'F' 'O' 'R' 'M'. 35 | /// -> 4 bytes indicating the size of the file 36 | /// -> 4 bytes have the ASCII characters 'A' 'I' 'F' 'F'. 37 | /// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/Docs/AIFF-1.3.pdf 38 | MagicNumber('audio/x-aiff', [ 39 | 0x46, 40 | 0x4F, 41 | 0x52, 42 | 0x4D, 43 | 0x00, 44 | 0x00, 45 | 0x00, 46 | 0x00, 47 | 0x41, 48 | 0x49, 49 | 0x46, 50 | 0x46 51 | ], mask: [ 52 | 0xFF, 53 | 0xFF, 54 | 0xFF, 55 | 0xFF, 56 | 0x00, 57 | 0x00, 58 | 0x00, 59 | 0x00, 60 | 0xFF, 61 | 0xFF, 62 | 0xFF, 63 | 0xFF 64 | ]), 65 | 66 | /// -> 4 bytes have the ASCII characters 'f' 'L' 'a' 'C'. 67 | /// https://xiph.org/flac/format.html 68 | MagicNumber('audio/x-flac', [0x66, 0x4C, 0x61, 0x43]), 69 | 70 | /// The WAVE file format is based on the RIFF document format. 71 | /// -> 4 bytes have the ASCII characters 'R' 'I' 'F' 'F'. 72 | /// -> 4 bytes indicating the size of the file 73 | /// -> 4 bytes have the ASCII characters 'W' 'A' 'V' 'E'. 74 | /// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf 75 | MagicNumber('audio/x-wav', [ 76 | 0x52, 77 | 0x49, 78 | 0x46, 79 | 0x46, 80 | 0x00, 81 | 0x00, 82 | 0x00, 83 | 0x00, 84 | 0x57, 85 | 0x41, 86 | 0x56, 87 | 0x45 88 | ], mask: [ 89 | 0xFF, 90 | 0xFF, 91 | 0xFF, 92 | 0xFF, 93 | 0x00, 94 | 0x00, 95 | 0x00, 96 | 0x00, 97 | 0xFF, 98 | 0xFF, 99 | 0xFF, 100 | 0xFF 101 | ]), 102 | MagicNumber('image/gif', [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]), 103 | MagicNumber('image/gif', [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]), 104 | MagicNumber('image/jpeg', [0xFF, 0xD8]), 105 | MagicNumber('image/png', [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), 106 | MagicNumber('image/tiff', [0x49, 0x49, 0x2A, 0x00]), 107 | MagicNumber('image/tiff', [0x4D, 0x4D, 0x00, 0x2A]), 108 | MagicNumber('audio/aac', [0xFF, 0xF1]), 109 | MagicNumber('audio/aac', [0xFF, 0xF9]), 110 | MagicNumber('audio/weba', [0x1A, 0x45, 0xDF, 0xA3]), 111 | MagicNumber('audio/mpeg', [0x49, 0x44, 0x33]), 112 | MagicNumber('audio/mpeg', [0xFF, 0xFB]), 113 | MagicNumber('audio/ogg', [0x4F, 0x70, 0x75]), 114 | MagicNumber('video/3gpp', [ 115 | 0x00, 116 | 0x00, 117 | 0x00, 118 | 0x00, 119 | 0x66, 120 | 0x74, 121 | 0x79, 122 | 0x70, 123 | 0x33, 124 | 0x67, 125 | 0x70, 126 | 0x35 127 | ], mask: [ 128 | 0xFF, 129 | 0xFF, 130 | 0xFF, 131 | 0x00, 132 | 0xFF, 133 | 0xFF, 134 | 0xFF, 135 | 0xFF, 136 | 0xFF, 137 | 0xFF, 138 | 0xFF, 139 | 0xFF 140 | ]), 141 | MagicNumber('video/mp4', [ 142 | 0x00, 143 | 0x00, 144 | 0x00, 145 | 0x00, 146 | 0x66, 147 | 0x74, 148 | 0x79, 149 | 0x70, 150 | 0x61, 151 | 0x76, 152 | 0x63, 153 | 0x31 154 | ], mask: [ 155 | 0x00, 156 | 0x00, 157 | 0x00, 158 | 0x00, 159 | 0xFF, 160 | 0xFF, 161 | 0xFF, 162 | 0xFF, 163 | 0xFF, 164 | 0xFF, 165 | 0xFF, 166 | 0xFF 167 | ]), 168 | MagicNumber('video/mp4', [ 169 | 0x00, 170 | 0x00, 171 | 0x00, 172 | 0x00, 173 | 0x66, 174 | 0x74, 175 | 0x79, 176 | 0x70, 177 | 0x69, 178 | 0x73, 179 | 0x6F, 180 | 0x32 181 | ], mask: [ 182 | 0x00, 183 | 0x00, 184 | 0x00, 185 | 0x00, 186 | 0xFF, 187 | 0xFF, 188 | 0xFF, 189 | 0xFF, 190 | 0xFF, 191 | 0xFF, 192 | 0xFF, 193 | 0xFF 194 | ]), 195 | MagicNumber('video/mp4', [ 196 | 0x00, 197 | 0x00, 198 | 0x00, 199 | 0x00, 200 | 0x66, 201 | 0x74, 202 | 0x79, 203 | 0x70, 204 | 0x69, 205 | 0x73, 206 | 0x6F, 207 | 0x6D 208 | ], mask: [ 209 | 0x00, 210 | 0x00, 211 | 0x00, 212 | 0x00, 213 | 0xFF, 214 | 0xFF, 215 | 0xFF, 216 | 0xFF, 217 | 0xFF, 218 | 0xFF, 219 | 0xFF, 220 | 0xFF 221 | ]), 222 | MagicNumber('video/mp4', [ 223 | 0x00, 224 | 0x00, 225 | 0x00, 226 | 0x00, 227 | 0x66, 228 | 0x74, 229 | 0x79, 230 | 0x70, 231 | 0x6D, 232 | 0x70, 233 | 0x34, 234 | 0x31 235 | ], mask: [ 236 | 0x00, 237 | 0x00, 238 | 0x00, 239 | 0x00, 240 | 0xFF, 241 | 0xFF, 242 | 0xFF, 243 | 0xFF, 244 | 0xFF, 245 | 0xFF, 246 | 0xFF, 247 | 0xFF 248 | ]), 249 | MagicNumber('video/mp4', [ 250 | 0x00, 251 | 0x00, 252 | 0x00, 253 | 0x00, 254 | 0x66, 255 | 0x74, 256 | 0x79, 257 | 0x70, 258 | 0x6D, 259 | 0x70, 260 | 0x34, 261 | 0x32 262 | ], mask: [ 263 | 0x00, 264 | 0x00, 265 | 0x00, 266 | 0x00, 267 | 0xFF, 268 | 0xFF, 269 | 0xFF, 270 | 0xFF, 271 | 0xFF, 272 | 0xFF, 273 | 0xFF, 274 | 0xFF 275 | ]), 276 | MagicNumber('model/gltf-binary', [0x46, 0x54, 0x6C, 0x67]), 277 | 278 | /// The WebP file format is based on the RIFF document format. 279 | /// -> 4 bytes have the ASCII characters 'R' 'I' 'F' 'F'. 280 | /// -> 4 bytes indicating the size of the file 281 | /// -> 4 bytes have the ASCII characters 'W' 'E' 'B' 'P'. 282 | /// https://developers.google.com/speed/webp/docs/riff_container 283 | MagicNumber('image/webp', [ 284 | 0x52, 285 | 0x49, 286 | 0x46, 287 | 0x46, 288 | 0x00, 289 | 0x00, 290 | 0x00, 291 | 0x00, 292 | 0x57, 293 | 0x45, 294 | 0x42, 295 | 0x50 296 | ], mask: [ 297 | 0xFF, 298 | 0xFF, 299 | 0xFF, 300 | 0xFF, 301 | 0x00, 302 | 0x00, 303 | 0x00, 304 | 0x00, 305 | 0xFF, 306 | 0xFF, 307 | 0xFF, 308 | 0xFF 309 | ]), 310 | 311 | MagicNumber('font/woff2', [0x77, 0x4f, 0x46, 0x32]), 312 | 313 | /// High Efficiency Image File Format (ISO/IEC 23008-12). 314 | /// -> 4 bytes indicating the ftyp box length. 315 | /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. 316 | /// -> 4 bytes have the ASCII characters 'h' 'e' 'i' 'c'. 317 | /// https://www.iana.org/assignments/media-types/image/heic 318 | MagicNumber('image/heic', [ 319 | 0x00, 320 | 0x00, 321 | 0x00, 322 | 0x00, 323 | 0x66, 324 | 0x74, 325 | 0x79, 326 | 0x70, 327 | 0x68, 328 | 0x65, 329 | 0x69, 330 | 0x63 331 | ], mask: [ 332 | 0x00, 333 | 0x00, 334 | 0x00, 335 | 0x00, 336 | 0xFF, 337 | 0xFF, 338 | 0xFF, 339 | 0xFF, 340 | 0xFF, 341 | 0xFF, 342 | 0xFF, 343 | 0xFF 344 | ]), 345 | 346 | /// -> 4 bytes indicating the ftyp box length. 347 | /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. 348 | /// -> 4 bytes have the ASCII characters 'h' 'e' 'i' 'x'. 349 | MagicNumber('image/heic', [ 350 | 0x00, 351 | 0x00, 352 | 0x00, 353 | 0x00, 354 | 0x66, 355 | 0x74, 356 | 0x79, 357 | 0x70, 358 | 0x68, 359 | 0x65, 360 | 0x69, 361 | 0x78 362 | ], mask: [ 363 | 0x00, 364 | 0x00, 365 | 0x00, 366 | 0x00, 367 | 0xFF, 368 | 0xFF, 369 | 0xFF, 370 | 0xFF, 371 | 0xFF, 372 | 0xFF, 373 | 0xFF, 374 | 0xFF 375 | ]), 376 | 377 | /// -> 4 bytes indicating the ftyp box length. 378 | /// -> 4 bytes have the ASCII characters 'f' 't' 'y' 'p'. 379 | /// -> 4 bytes have the ASCII characters 'm' 'i' 'f' '1'. 380 | MagicNumber('image/heif', [ 381 | 0x00, 382 | 0x00, 383 | 0x00, 384 | 0x00, 385 | 0x66, 386 | 0x74, 387 | 0x79, 388 | 0x70, 389 | 0x6D, 390 | 0x69, 391 | 0x66, 392 | 0x31 393 | ], mask: [ 394 | 0x00, 395 | 0x00, 396 | 0x00, 397 | 0x00, 398 | 0xFF, 399 | 0xFF, 400 | 0xFF, 401 | 0xFF, 402 | 0xFF, 403 | 0xFF, 404 | 0xFF, 405 | 0xFF 406 | ]), 407 | ]; 408 | -------------------------------------------------------------------------------- /lib/src/mime_multipart_transformer.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:typed_data'; 7 | 8 | import 'bound_multipart_stream.dart'; 9 | import 'char_code.dart' as char_code; 10 | import 'mime_shared.dart'; 11 | 12 | Uint8List _getBoundary(String boundary) { 13 | final charCodes = boundary.codeUnits; 14 | 15 | final boundaryList = Uint8List(4 + charCodes.length); 16 | // Set-up the matching boundary preceding it with CRLF and two 17 | // dashes. 18 | boundaryList[0] = char_code.cr; 19 | boundaryList[1] = char_code.lf; 20 | boundaryList[2] = char_code.dash; 21 | boundaryList[3] = char_code.dash; 22 | boundaryList.setRange(4, 4 + charCodes.length, charCodes); 23 | return boundaryList; 24 | } 25 | 26 | /// Parser for MIME multipart types of data as described in RFC 2046 27 | /// section 5.1.1. The data is transformed into [MimeMultipart] objects, each 28 | /// of them streaming the multipart data. 29 | class MimeMultipartTransformer 30 | extends StreamTransformerBase, MimeMultipart> { 31 | final List _boundary; 32 | 33 | /// Construct a new MIME multipart parser with the boundary 34 | /// [boundary]. The boundary should be as specified in the content 35 | /// type parameter, that is without the -- prefix. 36 | MimeMultipartTransformer(String boundary) 37 | : _boundary = _getBoundary(boundary); 38 | 39 | @override 40 | Stream bind(Stream> stream) => 41 | BoundMultipartStream(_boundary, stream).stream; 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/mime_shared.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'mime_multipart_transformer.dart'; 6 | 7 | class MimeMultipartException implements Exception { 8 | final String message; 9 | 10 | const MimeMultipartException([this.message = '']); 11 | 12 | @override 13 | String toString() => 'MimeMultipartException: $message'; 14 | } 15 | 16 | /// A Mime Multipart class representing each part parsed by 17 | /// [MimeMultipartTransformer]. The data is streamed in as it become available. 18 | abstract class MimeMultipart extends Stream> { 19 | Map get headers; 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/mime_type.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'default_extension_map.dart'; 6 | import 'magic_number.dart'; 7 | 8 | final MimeTypeResolver _globalResolver = MimeTypeResolver(); 9 | 10 | /// The maximum number of bytes needed, to match all default magic-numbers. 11 | int get defaultMagicNumbersMaxLength => _globalResolver.magicNumbersMaxLength; 12 | 13 | /// Extract the extension from [path] and use that for MIME-type lookup, using 14 | /// the default extension map. 15 | /// 16 | /// If no matching MIME-type was found, `null` is returned. 17 | /// 18 | /// If [headerBytes] is present, a match for known magic-numbers will be 19 | /// performed first. This allows the correct mime-type to be found, even though 20 | /// a file have been saved using the wrong file-name extension. If less than 21 | /// [defaultMagicNumbersMaxLength] bytes was provided, some magic-numbers won't 22 | /// be matched against. 23 | String? lookupMimeType(String path, {List? headerBytes}) => 24 | _globalResolver.lookup(path, headerBytes: headerBytes); 25 | 26 | /// Returns the extension for the given MIME type. 27 | /// 28 | /// If there are multiple extensions for [mime], return the first occurrence in 29 | /// the map. If there are no extensions for [mime], return [mime]. 30 | String extensionFromMime(String mime) { 31 | mime = mime.toLowerCase(); 32 | for (final entry in defaultExtensionMap.entries) { 33 | if (defaultExtensionMap[entry.key] == mime) { 34 | return entry.key; 35 | } 36 | } 37 | return mime; 38 | } 39 | 40 | /// MIME-type resolver class, used to customize the lookup of mime-types. 41 | class MimeTypeResolver { 42 | final Map _extensionMap = {}; 43 | final List _magicNumbers = []; 44 | final bool _useDefault; 45 | int _magicNumbersMaxLength; 46 | 47 | /// Create a new empty [MimeTypeResolver]. 48 | MimeTypeResolver.empty() 49 | : _useDefault = false, 50 | _magicNumbersMaxLength = 0; 51 | 52 | /// Create a new [MimeTypeResolver] containing the default scope. 53 | MimeTypeResolver() 54 | : _useDefault = true, 55 | _magicNumbersMaxLength = initialMagicNumbersMaxLength; 56 | 57 | /// Get the maximum number of bytes required to match all magic numbers, when 58 | /// performing [lookup] with headerBytes present. 59 | int get magicNumbersMaxLength => _magicNumbersMaxLength; 60 | 61 | /// Extract the extension from [path] and use that for MIME-type lookup. 62 | /// 63 | /// If no matching MIME-type was found, `null` is returned. 64 | /// 65 | /// If [headerBytes] is present, a match for known magic-numbers will be 66 | /// performed first. This allows the correct mime-type to be found, even 67 | /// though a file have been saved using the wrong file-name extension. If less 68 | /// than [magicNumbersMaxLength] bytes was provided, some magic-numbers won't 69 | /// be matched against. 70 | String? lookup(String path, {List? headerBytes}) { 71 | String? result; 72 | if (headerBytes != null) { 73 | result = _matchMagic(headerBytes, _magicNumbers); 74 | if (result != null) return result; 75 | if (_useDefault) { 76 | result = _matchMagic(headerBytes, initialMagicNumbers); 77 | if (result != null) return result; 78 | } 79 | } 80 | final ext = _ext(path); 81 | result = _extensionMap[ext]; 82 | if (result != null) return result; 83 | if (_useDefault) { 84 | result = defaultExtensionMap[ext]; 85 | if (result != null) return result; 86 | } 87 | return null; 88 | } 89 | 90 | /// Add a new MIME-type mapping to the [MimeTypeResolver]. If the [extension] 91 | /// is already present in the [MimeTypeResolver], it'll be overwritten. 92 | void addExtension(String extension, String mimeType) { 93 | _extensionMap[extension] = mimeType; 94 | } 95 | 96 | /// Add a new magic-number mapping to the [MimeTypeResolver]. 97 | /// 98 | /// If [mask] is present,the [mask] is used to only perform matching on 99 | /// selective bits. The [mask] must have the same length as [bytes]. 100 | void addMagicNumber(List bytes, String mimeType, {List? mask}) { 101 | if (mask != null && bytes.length != mask.length) { 102 | throw ArgumentError('Bytes and mask are of different lengths'); 103 | } 104 | if (bytes.length > _magicNumbersMaxLength) { 105 | _magicNumbersMaxLength = bytes.length; 106 | } 107 | _magicNumbers.add(MagicNumber(mimeType, bytes, mask: mask)); 108 | } 109 | 110 | static String? _matchMagic( 111 | List headerBytes, List magicNumbers) { 112 | for (var mn in magicNumbers) { 113 | if (mn.matches(headerBytes)) return mn.mimeType; 114 | } 115 | return null; 116 | } 117 | 118 | static String _ext(String path) { 119 | final index = path.lastIndexOf('.'); 120 | if (index < 0 || index + 1 >= path.length) return path; 121 | return path.substring(index + 1).toLowerCase(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mime 2 | version: 1.0.6-wip 3 | description: >- 4 | Utilities for handling media (MIME) types, including determining a type from 5 | a file extension and file contents. 6 | repository: https://github.com/dart-lang/mime 7 | topics: 8 | - magic-numbers 9 | - mime 10 | - mimetype 11 | - multipart-form 12 | 13 | environment: 14 | sdk: ^3.2.0 15 | 16 | dev_dependencies: 17 | dart_flutter_team_lints: ^3.0.0 18 | test: ^1.16.6 19 | -------------------------------------------------------------------------------- /test/default_extension_map_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:mime/src/default_extension_map.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('defaultExtensionMap', () { 10 | test('keys are lowercase', () { 11 | for (final key in defaultExtensionMap.keys) { 12 | expect(key, equals(key.toLowerCase())); 13 | } 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/mime_multipart_transformer_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:math'; 7 | 8 | import 'package:mime/mime.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void _writeInChunks( 12 | List data, int chunkSize, StreamController> controller) { 13 | if (chunkSize == -1) chunkSize = data.length; 14 | 15 | for (var pos = 0; pos < data.length; pos += chunkSize) { 16 | final remaining = data.length - pos; 17 | final writeLength = min(chunkSize, remaining); 18 | controller.add(data.sublist(pos, pos + writeLength)); 19 | } 20 | controller.close(); 21 | } 22 | 23 | enum TestMode { immediateListen, delayListen, pauseResume } 24 | 25 | void _runParseTest( 26 | String message, 27 | String boundary, 28 | TestMode mode, [ 29 | List>? expectedHeaders, 30 | List? expectedParts, 31 | bool expectError = false, 32 | ]) { 33 | Future testWrite(List data, [int chunkSize = -1]) { 34 | final controller = StreamController>(sync: true); 35 | 36 | final stream = 37 | controller.stream.transform(MimeMultipartTransformer(boundary)); 38 | var i = 0; 39 | final completer = Completer(); 40 | final futures = >[]; 41 | stream.listen((multipart) { 42 | final part = i++; 43 | if (expectedHeaders != null) { 44 | expect(multipart.headers, equals(expectedHeaders[part])); 45 | } 46 | switch (mode) { 47 | case TestMode.immediateListen: 48 | futures.add(multipart.fold>( 49 | [], (buffer, data) => buffer..addAll(data)).then((data) { 50 | if (expectedParts?[part] != null) { 51 | expect(data, equals(expectedParts?[part]!.codeUnits)); 52 | } 53 | })); 54 | 55 | case TestMode.delayListen: 56 | futures.add( 57 | Future( 58 | () => multipart.fold>( 59 | [], 60 | (buffer, data) => buffer..addAll(data), 61 | ).then( 62 | (data) { 63 | if (expectedParts?[part] != null) { 64 | expect(data, equals(expectedParts?[part]!.codeUnits)); 65 | } 66 | }, 67 | ), 68 | ), 69 | ); 70 | 71 | case TestMode.pauseResume: 72 | final completer = Completer(); 73 | futures.add(completer.future); 74 | final buffer = []; 75 | late StreamSubscription> subscription; 76 | subscription = multipart.listen((data) { 77 | buffer.addAll(data); 78 | subscription.pause(); 79 | Future(() => subscription.resume()); 80 | }, onDone: () { 81 | if (expectedParts?[part] != null) { 82 | expect(buffer, equals(expectedParts?[part]!.codeUnits)); 83 | } 84 | completer.complete(); 85 | }); 86 | addTearDown(subscription.cancel); 87 | } 88 | }, onError: (Object error) { 89 | // ignore: only_throw_errors 90 | if (!expectError) throw error; 91 | }, onDone: () { 92 | if (expectedParts != null) { 93 | expect(i, equals(expectedParts.length)); 94 | } 95 | Future.wait(futures).then(completer.complete); 96 | }); 97 | 98 | _writeInChunks(data, chunkSize, controller); 99 | 100 | return completer.future; 101 | } 102 | 103 | Future testFirstPartOnly(List data, [int chunkSize = -1]) { 104 | final completer = Completer(); 105 | final controller = StreamController>(sync: true); 106 | 107 | final stream = 108 | controller.stream.transform(MimeMultipartTransformer(boundary)); 109 | 110 | stream.first.then((multipart) { 111 | if (expectedHeaders != null) { 112 | expect(multipart.headers, equals(expectedHeaders[0])); 113 | } 114 | return multipart.fold>([], (b, d) => b..addAll(d)).then( 115 | (data) { 116 | if (expectedParts != null && expectedParts[0] != null) { 117 | expect(data, equals(expectedParts[0]!.codeUnits)); 118 | } 119 | }, 120 | ); 121 | }).then((_) { 122 | completer.complete(); 123 | }); 124 | 125 | _writeInChunks(data, chunkSize, controller); 126 | 127 | return completer.future; 128 | } 129 | 130 | Future testCompletePartAfterCancel(List data, int parts, 131 | [int chunkSize = -1]) { 132 | final completer = Completer(); 133 | final controller = StreamController>(sync: true); 134 | final stream = 135 | controller.stream.transform(MimeMultipartTransformer(boundary)); 136 | late StreamSubscription subscription; 137 | var i = 0; 138 | final futures = >[]; 139 | subscription = stream.listen((multipart) { 140 | final partIndex = i; 141 | 142 | if (partIndex >= parts) { 143 | throw StateError('Expected no more parts, but got one.'); 144 | } 145 | 146 | if (expectedHeaders != null) { 147 | expect(multipart.headers, equals(expectedHeaders[partIndex])); 148 | } 149 | futures.add( 150 | multipart.fold>([], (b, d) => b..addAll(d)).then((data) { 151 | if (expectedParts != null && expectedParts[partIndex] != null) { 152 | expect(data, equals(expectedParts[partIndex]!.codeUnits)); 153 | } 154 | })); 155 | 156 | if (partIndex == (parts - 1)) { 157 | subscription.cancel(); 158 | Future.wait(futures).then(completer.complete); 159 | } 160 | i++; 161 | }); 162 | 163 | _writeInChunks(data, chunkSize, controller); 164 | 165 | return completer.future; 166 | } 167 | 168 | // Test parsing the data three times delivering the data in 169 | // different chunks. 170 | final data = message.codeUnits; 171 | test('test', () { 172 | expect( 173 | Future.wait([ 174 | testWrite(data), 175 | testWrite(data, 10), 176 | testWrite(data, 2), 177 | testWrite(data, 1), 178 | ]), 179 | completes); 180 | }); 181 | 182 | if (expectedParts!.isNotEmpty) { 183 | test('test-first-part-only', () { 184 | expect( 185 | Future.wait([ 186 | testFirstPartOnly(data), 187 | testFirstPartOnly(data, 10), 188 | testFirstPartOnly(data, 2), 189 | testFirstPartOnly(data, 1), 190 | ]), 191 | completes); 192 | }); 193 | 194 | test('test-n-parts-only', () { 195 | var numPartsExpected = expectedParts.length - 1; 196 | if (numPartsExpected == 0) numPartsExpected = 1; 197 | 198 | expect( 199 | Future.wait([ 200 | testCompletePartAfterCancel(data, numPartsExpected), 201 | testCompletePartAfterCancel(data, numPartsExpected, 10), 202 | testCompletePartAfterCancel(data, numPartsExpected, 2), 203 | testCompletePartAfterCancel(data, numPartsExpected, 1), 204 | ]), 205 | completes); 206 | }); 207 | } 208 | } 209 | 210 | void _testParse(String message, String boundary, 211 | [List>? expectedHeaders, 212 | List? expectedParts, 213 | bool expectError = false]) { 214 | _runParseTest(message, boundary, TestMode.immediateListen, expectedHeaders, 215 | expectedParts, expectError); 216 | _runParseTest(message, boundary, TestMode.delayListen, expectedHeaders, 217 | expectedParts, expectError); 218 | _runParseTest(message, boundary, TestMode.pauseResume, expectedHeaders, 219 | expectedParts, expectError); 220 | } 221 | 222 | void _testParseValid() { 223 | // Empty message from Chrome form post. 224 | var message = '------WebKitFormBoundaryU3FBruSkJKG0Yor1--\r\n'; 225 | _testParse(message, '----WebKitFormBoundaryU3FBruSkJKG0Yor1', [], []); 226 | 227 | // Sample from Wikipedia. 228 | message = ''' 229 | This is a message with multiple parts in MIME format.\r 230 | --frontier\r 231 | Content-Type: text/plain\r 232 | \r 233 | This is the body of the message.\r 234 | --frontier\r 235 | Content-Type: application/octet-stream\r 236 | Content-Transfer-Encoding: base64\r 237 | \r 238 | PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg 239 | Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg=\r 240 | --frontier--\r\n'''; 241 | var headers1 = {'content-type': 'text/plain'}; 242 | var headers2 = { 243 | 'content-type': 'application/octet-stream', 244 | 'content-transfer-encoding': 'base64' 245 | }; 246 | var body1 = 'This is the body of the message.'; 247 | var body2 = ''' 248 | PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg 249 | Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg='''; 250 | _testParse(message, 'frontier', [headers1, headers2], [body1, body2]); 251 | 252 | // Sample from HTML 4.01 Specification. 253 | message = ''' 254 | \r\n--AaB03x\r 255 | Content-Disposition: form-data; name="submit-name"\r 256 | \r 257 | Larry\r 258 | --AaB03x\r 259 | Content-Disposition: form-data; name="files"; filename="file1.txt"\r 260 | Content-Type: text/plain\r 261 | \r 262 | ... contents of file1.txt ...\r 263 | --AaB03x--\r\n'''; 264 | headers1 = { 265 | 'content-disposition': 'form-data; name="submit-name"' 266 | }; 267 | headers2 = { 268 | 'content-type': 'text/plain', 269 | 'content-disposition': 'form-data; name="files"; filename="file1.txt"' 270 | }; 271 | body1 = 'Larry'; 272 | body2 = '... contents of file1.txt ...'; 273 | _testParse(message, 'AaB03x', [headers1, headers2], [body1, body2]); 274 | 275 | // Longer form from submitting the following from Chrome. 276 | // 277 | // 278 | // 279 | //
282 | //

283 | // Text: 284 | // Password: 285 | // Checkbox: 286 | // Radio: 287 | // Send 288 | //

289 | //
290 | // 291 | // 292 | 293 | message = ''' 294 | \r\n------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r 295 | Content-Disposition: form-data; name="text_input"\r 296 | \r 297 | text\r 298 | ------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r 299 | Content-Disposition: form-data; name="password_input"\r 300 | \r 301 | password\r 302 | ------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r 303 | Content-Disposition: form-data; name="checkbox_input"\r 304 | \r 305 | on\r 306 | ------WebKitFormBoundaryQ3cgYAmGRF8yOeYB\r 307 | Content-Disposition: form-data; name="radio_input"\r 308 | \r 309 | on\r 310 | ------WebKitFormBoundaryQ3cgYAmGRF8yOeYB--\r\n'''; 311 | headers1 = { 312 | 'content-disposition': 'form-data; name="text_input"' 313 | }; 314 | headers2 = { 315 | 'content-disposition': 'form-data; name="password_input"' 316 | }; 317 | final headers3 = { 318 | 'content-disposition': 'form-data; name="checkbox_input"' 319 | }; 320 | final headers4 = { 321 | 'content-disposition': 'form-data; name="radio_input"' 322 | }; 323 | body1 = 'text'; 324 | body2 = 'password'; 325 | const body3 = 'on'; 326 | const body4 = 'on'; 327 | _testParse(message, '----WebKitFormBoundaryQ3cgYAmGRF8yOeYB', 328 | [headers1, headers2, headers3, headers4], [body1, body2, body3, body4]); 329 | 330 | // Same form from Firefox. 331 | message = ''' 332 | \r\n-----------------------------52284550912143824192005403738\r 333 | Content-Disposition: form-data; name="text_input"\r 334 | \r 335 | text\r 336 | -----------------------------52284550912143824192005403738\r 337 | Content-Disposition: form-data; name="password_input"\r 338 | \r 339 | password\r 340 | -----------------------------52284550912143824192005403738\r 341 | Content-Disposition: form-data; name="checkbox_input"\r 342 | \r 343 | on\r 344 | -----------------------------52284550912143824192005403738\r 345 | Content-Disposition: form-data; name="radio_input"\r 346 | \r 347 | on\r 348 | -----------------------------52284550912143824192005403738--\r\n'''; 349 | _testParse( 350 | message, 351 | '---------------------------52284550912143824192005403738', 352 | [headers1, headers2, headers3, headers4], 353 | [body1, body2, body3, body4]); 354 | 355 | // And Internet Explorer 356 | message = ''' 357 | \r\n-----------------------------7dc8f38c60326\r 358 | Content-Disposition: form-data; name="text_input"\r 359 | \r 360 | text\r 361 | -----------------------------7dc8f38c60326\r 362 | Content-Disposition: form-data; name="password_input"\r 363 | \r 364 | password\r 365 | -----------------------------7dc8f38c60326\r 366 | Content-Disposition: form-data; name="checkbox_input"\r 367 | \r 368 | on\r 369 | -----------------------------7dc8f38c60326\r 370 | Content-Disposition: form-data; name="radio_input"\r 371 | \r 372 | on\r 373 | -----------------------------7dc8f38c60326--\r\n'''; 374 | _testParse(message, '---------------------------7dc8f38c60326', 375 | [headers1, headers2, headers3, headers4], [body1, body2, body3, body4]); 376 | 377 | // Test boundary prefix inside prefix and content. 378 | message = ''' 379 | -\r 380 | --\r 381 | --b\r 382 | --bo\r 383 | --bou\r 384 | --boun\r 385 | --bound\r 386 | --bounda\r 387 | --boundar\r 388 | --boundary\r 389 | Content-Type: text/plain\r 390 | \r 391 | -\r 392 | --\r 393 | --b\r 394 | --bo\r 395 | --bou\r 396 | --boun\r 397 | --bound\r\r 398 | --bounda\r\r\r 399 | --boundar\r\r\r\r 400 | --boundary\r 401 | Content-Type: text/plain\r 402 | \r 403 | --boundar\r 404 | --bounda\r 405 | --bound\r 406 | --boun\r 407 | --bou\r 408 | --bo\r 409 | --b\r\r\r\r 410 | --\r\r\r 411 | -\r\r 412 | --boundary--\r\n'''; 413 | final headers = {'content-type': 'text/plain'}; 414 | body1 = ''' 415 | -\r 416 | --\r 417 | --b\r 418 | --bo\r 419 | --bou\r 420 | --boun\r 421 | --bound\r\r 422 | --bounda\r\r\r 423 | --boundar\r\r\r'''; 424 | body2 = ''' 425 | --boundar\r 426 | --bounda\r 427 | --bound\r 428 | --boun\r 429 | --bou\r 430 | --bo\r 431 | --b\r\r\r\r 432 | --\r\r\r 433 | -\r'''; 434 | _testParse(message, 'boundary', [headers, headers], [body1, body2]); 435 | 436 | // Without initial CRLF. 437 | message = ''' 438 | --xxx\r 439 | \r 440 | \r 441 | Body 1\r 442 | --xxx\r 443 | \r 444 | \r 445 | Body2\r 446 | --xxx--\r\n'''; 447 | _testParse(message, 'xxx', null, ['\r\nBody 1', '\r\nBody2']); 448 | } 449 | 450 | void _testParseInvalid() { 451 | // Missing end boundary. 452 | const message = ''' 453 | \r 454 | --xxx\r 455 | \r 456 | \r 457 | Body 1\r 458 | --xxx\r 459 | \r 460 | \r 461 | Body2\r 462 | --xxx\r\n'''; 463 | _testParse(message, 'xxx', null, [null, null], true); 464 | } 465 | 466 | void main() { 467 | _testParseValid(); 468 | _testParseInvalid(); 469 | } 470 | -------------------------------------------------------------------------------- /test/mime_type_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import 'package:mime/mime.dart'; 8 | import 'package:mime/src/magic_number.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | void _expectMimeType(String path, String? expectedMimeType, 12 | {List? headerBytes, MimeTypeResolver? resolver}) { 13 | String? mimeType; 14 | if (resolver == null) { 15 | mimeType = lookupMimeType(path, headerBytes: headerBytes); 16 | } else { 17 | mimeType = resolver.lookup(path, headerBytes: headerBytes); 18 | } 19 | 20 | expect(mimeType, expectedMimeType); 21 | } 22 | 23 | void main() { 24 | group('global-lookup', () { 25 | test('by-path', () { 26 | _expectMimeType('file.dart', 'text/x-dart'); 27 | // Test mixed-case 28 | _expectMimeType('file.DaRT', 'text/x-dart'); 29 | _expectMimeType('file.dcm', 'application/dicom'); 30 | _expectMimeType('file.html', 'text/html'); 31 | _expectMimeType('file.xhtml', 'application/xhtml+xml'); 32 | _expectMimeType('file.jpeg', 'image/jpeg'); 33 | _expectMimeType('file.jpg', 'image/jpeg'); 34 | _expectMimeType('file.png', 'image/png'); 35 | _expectMimeType('file.gif', 'image/gif'); 36 | _expectMimeType('file.cc', 'text/x-c'); 37 | _expectMimeType('file.c', 'text/x-c'); 38 | _expectMimeType('file.css', 'text/css'); 39 | _expectMimeType('file.js', 'text/javascript'); 40 | _expectMimeType('file.mjs', 'text/javascript'); 41 | _expectMimeType('file.ps', 'application/postscript'); 42 | _expectMimeType('file.pdf', 'application/pdf'); 43 | _expectMimeType('file.tiff', 'image/tiff'); 44 | _expectMimeType('file.tif', 'image/tiff'); 45 | _expectMimeType('file.webp', 'image/webp'); 46 | _expectMimeType('file.mp3', 'audio/mpeg'); 47 | _expectMimeType('file.aac', 'audio/aac'); 48 | _expectMimeType('file.ogg', 'audio/ogg'); 49 | _expectMimeType('file.aiff', 'audio/x-aiff'); 50 | _expectMimeType('file.m4a', 'audio/mp4'); 51 | _expectMimeType('file.m4b', 'audio/mp4'); 52 | _expectMimeType('file.toml', 'application/toml'); 53 | _expectMimeType('file.md', 'text/markdown'); 54 | _expectMimeType('file.markdown', 'text/markdown'); 55 | _expectMimeType('file.heif', 'image/heif'); 56 | _expectMimeType('file.heic', 'image/heic'); 57 | }); 58 | 59 | test('unknown-mime-type', () { 60 | _expectMimeType('file.unsupported-extension', null); 61 | }); 62 | 63 | test('by-header-bytes', () { 64 | _expectMimeType('file.jpg', 'image/png', 65 | headerBytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); 66 | _expectMimeType('file.jpg', 'image/gif', headerBytes: [ 67 | 0x47, 68 | 0x49, 69 | 0x46, 70 | 0x38, 71 | 0x39, 72 | 0x61, 73 | 0x0D, 74 | 0x0A, 75 | 0x1A, 76 | 0x0A 77 | ]); 78 | _expectMimeType('file.gif', 'image/jpeg', headerBytes: [ 79 | 0xFF, 80 | 0xD8, 81 | 0x46, 82 | 0x38, 83 | 0x39, 84 | 0x61, 85 | 0x0D, 86 | 0x0A, 87 | 0x1A, 88 | 0x0A 89 | ]); 90 | _expectMimeType('file', 'video/3gpp', headerBytes: [ 91 | 0x00, 92 | 0x00, 93 | 0x00, 94 | 0x04, 95 | 0x66, 96 | 0x74, 97 | 0x79, 98 | 0x70, 99 | 0x33, 100 | 0x67, 101 | 0x70, 102 | 0x35 103 | ]); 104 | _expectMimeType('file.mp4', 'video/mp4', headerBytes: [ 105 | 0x00, 106 | 0x00, 107 | 0x00, 108 | 0x04, 109 | 0x66, 110 | 0x74, 111 | 0x79, 112 | 0x70, 113 | 0xFF, 114 | 0xFF, 115 | 0xFF, 116 | 0xFF 117 | ]); 118 | _expectMimeType('file', 'video/mp4', headerBytes: [ 119 | 0x00, 120 | 0xF0, 121 | 0xF0, 122 | 0xF0, 123 | 0x66, 124 | 0x74, 125 | 0x79, 126 | 0x70, 127 | 0x61, 128 | 0x76, 129 | 0x63, 130 | 0x31 131 | ]); 132 | _expectMimeType('file', 'video/mp4', headerBytes: [ 133 | 0x00, 134 | 0xF0, 135 | 0xF0, 136 | 0xF0, 137 | 0x66, 138 | 0x74, 139 | 0x79, 140 | 0x70, 141 | 0x69, 142 | 0x73, 143 | 0x6F, 144 | 0x32 145 | ]); 146 | _expectMimeType('file', 'video/mp4', headerBytes: [ 147 | 0x00, 148 | 0xF0, 149 | 0xF0, 150 | 0xF0, 151 | 0x66, 152 | 0x74, 153 | 0x79, 154 | 0x70, 155 | 0x69, 156 | 0x73, 157 | 0x6F, 158 | 0x6D 159 | ]); 160 | _expectMimeType('file', 'video/mp4', headerBytes: [ 161 | 0x00, 162 | 0xF0, 163 | 0xF0, 164 | 0xF0, 165 | 0x66, 166 | 0x74, 167 | 0x79, 168 | 0x70, 169 | 0x6D, 170 | 0x70, 171 | 0x34, 172 | 0x31 173 | ]); 174 | _expectMimeType('file', 'video/mp4', headerBytes: [ 175 | 0x00, 176 | 0xF0, 177 | 0xF0, 178 | 0xF0, 179 | 0x66, 180 | 0x74, 181 | 0x79, 182 | 0x70, 183 | 0x6D, 184 | 0x70, 185 | 0x34, 186 | 0x32 187 | ]); 188 | _expectMimeType('file', 'image/webp', headerBytes: [ 189 | 0x52, 190 | 0x49, 191 | 0x46, 192 | 0x46, 193 | 0xE2, 194 | 0x4A, 195 | 0x01, 196 | 0x00, 197 | 0x57, 198 | 0x45, 199 | 0x42, 200 | 0x50 201 | ]); 202 | _expectMimeType('file', 'audio/mpeg', 203 | headerBytes: [0x49, 0x44, 0x33, 0x0D, 0x0A, 0x1A, 0x0A]); 204 | _expectMimeType('file', 'audio/aac', 205 | headerBytes: [0xFF, 0xF1, 0x0D, 0x0A, 0x1A, 0x0A]); 206 | _expectMimeType('file', 'audio/ogg', 207 | headerBytes: [0x4F, 0x70, 0x75, 0x0D, 0x0A, 0x1A, 0x0A]); 208 | _expectMimeType('file', 'audio/x-aiff', headerBytes: [ 209 | 0x46, 210 | 0x4F, 211 | 0x52, 212 | 0x4D, 213 | 0x04, 214 | 0x0B, 215 | 0xEF, 216 | 0xF4, 217 | 0x41, 218 | 0x49, 219 | 0x46, 220 | 0x46 221 | ]); 222 | _expectMimeType('file', 'audio/x-flac', 223 | headerBytes: [0x66, 0x4C, 0x61, 0x43]); 224 | _expectMimeType('file', 'audio/x-wav', headerBytes: [ 225 | 0x52, 226 | 0x49, 227 | 0x46, 228 | 0x46, 229 | 0xA6, 230 | 0x4E, 231 | 0x70, 232 | 0x03, 233 | 0x57, 234 | 0x41, 235 | 0x56, 236 | 0x45 237 | ]); 238 | _expectMimeType('file', 'image/heic', headerBytes: [ 239 | 0x00, 240 | 0x00, 241 | 0x00, 242 | 0x18, 243 | 0x66, 244 | 0x74, 245 | 0x79, 246 | 0x70, 247 | 0x68, 248 | 0x65, 249 | 0x69, 250 | 0x63, 251 | 0x00 252 | ]); 253 | _expectMimeType('file', 'image/heic', headerBytes: [ 254 | 0x00, 255 | 0x00, 256 | 0x00, 257 | 0x18, 258 | 0x66, 259 | 0x74, 260 | 0x79, 261 | 0x70, 262 | 0x68, 263 | 0x65, 264 | 0x69, 265 | 0x78, 266 | 0x00 267 | ]); 268 | _expectMimeType('file', 'image/heif', headerBytes: [ 269 | 0x00, 270 | 0x00, 271 | 0x00, 272 | 0x18, 273 | 0x66, 274 | 0x74, 275 | 0x79, 276 | 0x70, 277 | 0x6D, 278 | 0x69, 279 | 0x66, 280 | 0x31, 281 | 0x00 282 | ]); 283 | }); 284 | }); 285 | 286 | group('custom-resolver', () { 287 | test('override-extension', () { 288 | final resolver = MimeTypeResolver(); 289 | resolver.addExtension('jpg', 'my-mime-type'); 290 | _expectMimeType('file.jpg', 'my-mime-type', resolver: resolver); 291 | }); 292 | 293 | test('fallthrough-extension', () { 294 | final resolver = MimeTypeResolver(); 295 | resolver.addExtension('jpg2', 'my-mime-type'); 296 | _expectMimeType('file.jpg', 'image/jpeg', resolver: resolver); 297 | }); 298 | 299 | test('with-mask', () { 300 | final resolver = MimeTypeResolver.empty(); 301 | resolver.addMagicNumber([0x01, 0x02, 0x03], 'my-mime-type', 302 | mask: [0x01, 0xFF, 0xFE]); 303 | _expectMimeType('file', 'my-mime-type', 304 | headerBytes: [0x01, 0x02, 0x03], resolver: resolver); 305 | _expectMimeType('file', null, 306 | headerBytes: [0x01, 0x03, 0x03], resolver: resolver); 307 | _expectMimeType('file', 'my-mime-type', 308 | headerBytes: [0xFF, 0x02, 0x02], resolver: resolver); 309 | }); 310 | }); 311 | 312 | test('default magic number', () { 313 | final actualMaxBytes = initialMagicNumbers.fold( 314 | 0, 315 | (previous, magic) => math.max(previous, magic.numbers.length), 316 | ); 317 | 318 | expect(initialMagicNumbersMaxLength, actualMaxBytes); 319 | }); 320 | 321 | group('extensionFromMime', () { 322 | test('returns match for mime with single extension', () { 323 | expect(extensionFromMime('application/json'), equals('json')); 324 | expect(extensionFromMime('application/java-archive'), equals('jar')); 325 | }); 326 | 327 | test('returns first match for mime with multiple extensions', () { 328 | expect(extensionFromMime('text/html'), equals('htm')); 329 | expect(extensionFromMime('application/x-cbr'), equals('cb7')); 330 | }); 331 | 332 | test('returns inputted string for unrecognized mime', () { 333 | expect( 334 | extensionFromMime('unrecognized_mime'), equals('unrecognized_mime')); 335 | expect(extensionFromMime('i/am/not/a/mime'), equals('i/am/not/a/mime')); 336 | }); 337 | }); 338 | } 339 | --------------------------------------------------------------------------------