Line data Source code
1 : // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 : // for details. All rights reserved. Use of this source code is governed by a 3 : // BSD-style license that can be found in the LICENSE file. 4 : 5 : import 'package:collection/collection.dart'; 6 : import 'package:string_scanner/string_scanner.dart'; 7 : 8 : import 'case_insensitive_map.dart'; 9 : import 'scan.dart'; 10 : import 'utils.dart'; 11 : 12 : /// A regular expression matching a character that needs to be backslash-escaped 13 : /// in a quoted string. 14 0 : final _escapedChar = RegExp(r'["\x00-\x1F\x7F]'); 15 : 16 : /// A class representing an HTTP media type, as used in Accept and Content-Type 17 : /// headers. 18 : /// 19 : /// This is immutable; new instances can be created based on an old instance by 20 : /// calling [change]. 21 : class MediaType { 22 : /// The primary identifier of the MIME type. 23 : /// 24 : /// This is always lowercase. 25 : final String type; 26 : 27 : /// The secondary identifier of the MIME type. 28 : /// 29 : /// This is always lowercase. 30 : final String subtype; 31 : 32 : /// The parameters to the media type. 33 : /// 34 : /// This map is immutable and the keys are case-insensitive. 35 : final Map<String, String> parameters; 36 : 37 : /// The media type's MIME type. 38 20 : String get mimeType => '$type/$subtype'; 39 : 40 : /// Parses a media type. 41 : /// 42 : /// This will throw a FormatError if the media type is invalid. 43 5 : factory MediaType.parse(String mediaType) => 44 : // This parsing is based on sections 3.6 and 3.7 of the HTTP spec: 45 : // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html. 46 10 : wrapFormatException('media type', mediaType, () { 47 5 : final scanner = StringScanner(mediaType); 48 10 : scanner.scan(whitespace); 49 10 : scanner.expect(token); 50 10 : final type = scanner.lastMatch![0]!; 51 5 : scanner.expect('/'); 52 10 : scanner.expect(token); 53 10 : final subtype = scanner.lastMatch![0]!; 54 10 : scanner.scan(whitespace); 55 : 56 5 : final parameters = <String, String>{}; 57 5 : while (scanner.scan(';')) { 58 10 : scanner.scan(whitespace); 59 10 : scanner.expect(token); 60 10 : final attribute = scanner.lastMatch![0]!; 61 5 : scanner.expect('='); 62 : 63 : String value; 64 10 : if (scanner.scan(token)) { 65 10 : value = scanner.lastMatch![0]!; 66 : } else { 67 0 : value = expectQuotedString(scanner); 68 : } 69 : 70 10 : scanner.scan(whitespace); 71 5 : parameters[attribute] = value; 72 : } 73 : 74 5 : scanner.expectDone(); 75 5 : return MediaType(type, subtype, parameters); 76 : }); 77 : 78 6 : MediaType(String type, String subtype, [Map<String, String>? parameters]) 79 6 : : type = type.toLowerCase(), 80 6 : subtype = subtype.toLowerCase(), 81 6 : parameters = UnmodifiableMapView( 82 7 : parameters == null ? {} : CaseInsensitiveMap.from(parameters)); 83 : 84 : /// Returns a copy of this [MediaType] with some fields altered. 85 : /// 86 : /// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed 87 : /// and alters both the [type] and [subtype] fields; it cannot be passed along 88 : /// with [type] or [subtype]. 89 : /// 90 : /// [parameters] overwrites and adds to the corresponding field. If 91 : /// [clearParameters] is passed, it replaces the corresponding field entirely 92 : /// instead. 93 1 : MediaType change( 94 : {String? type, 95 : String? subtype, 96 : String? mimeType, 97 : Map<String, String>? parameters, 98 : bool clearParameters = false}) { 99 : if (mimeType != null) { 100 : if (type != null) { 101 0 : throw ArgumentError('You may not pass both [type] and [mimeType].'); 102 : } else if (subtype != null) { 103 0 : throw ArgumentError('You may not pass both [subtype] and ' 104 : '[mimeType].'); 105 : } 106 : 107 0 : final segments = mimeType.split('/'); 108 0 : if (segments.length != 2) { 109 0 : throw FormatException('Invalid mime type "$mimeType".'); 110 : } 111 : 112 0 : type = segments[0]; 113 0 : subtype = segments[1]; 114 : } 115 : 116 1 : type ??= this.type; 117 1 : subtype ??= this.subtype; 118 0 : parameters ??= {}; 119 : 120 : if (!clearParameters) { 121 : final newParameters = parameters; 122 2 : parameters = Map.from(this.parameters); 123 1 : parameters.addAll(newParameters); 124 : } 125 : 126 1 : return MediaType(type, subtype, parameters); 127 : } 128 : 129 : /// Converts the media type to a string. 130 : /// 131 : /// This will produce a valid HTTP media type. 132 1 : @override 133 : String toString() { 134 6 : final buffer = StringBuffer()..write(type)..write('/')..write(subtype); 135 : 136 3 : parameters.forEach((attribute, value) { 137 2 : buffer.write('; $attribute='); 138 2 : if (nonToken.hasMatch(value)) { 139 : buffer 140 0 : ..write('"') 141 0 : ..write( 142 0 : value.replaceAllMapped(_escapedChar, (match) => '\\${match[0]}')) 143 0 : ..write('"'); 144 : } else { 145 1 : buffer.write(value); 146 : } 147 : }); 148 : 149 1 : return buffer.toString(); 150 : } 151 : }