blob: 59e692887b3bfab1d4eff0c2810986d18c999a6b [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:string_scanner/string_scanner.dart';
import 'case_insensitive_map.dart';
import 'scan.dart';
import 'utils.dart';
/// A regular expression matching a character that needs to be backslash-escaped
/// in a quoted string.
final _escapedChar = RegExp(r'["\x00-\x1F\x7F]');
/// A class representing an HTTP media type, as used in Accept and Content-Type
/// headers.
///
/// This is immutable; new instances can be created based on an old instance by
/// calling [change].
class MediaType {
/// The primary identifier of the MIME type.
///
/// This is always lowercase.
final String type;
/// The secondary identifier of the MIME type.
///
/// This is always lowercase.
final String subtype;
/// The parameters to the media type.
///
/// This map is immutable and the keys are case-insensitive.
final Map<String, String> parameters;
/// The media type's MIME type.
String get mimeType => '$type/$subtype';
/// Parses a media type.
///
/// This will throw a FormatError if the media type is invalid.
factory MediaType.parse(String mediaType) =>
// This parsing is based on sections 3.6 and 3.7 of the HTTP spec:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html.
wrapFormatException('media type', mediaType, () {
final scanner = StringScanner(mediaType);
scanner.scan(whitespace);
scanner.expect(token);
final type = scanner.lastMatch![0]!;
scanner.expect('/');
scanner.expect(token);
final subtype = scanner.lastMatch![0]!;
scanner.scan(whitespace);
final parameters = <String, String>{};
while (scanner.scan(';')) {
scanner.scan(whitespace);
scanner.expect(token);
final attribute = scanner.lastMatch![0]!;
scanner.expect('=');
String value;
if (scanner.scan(token)) {
value = scanner.lastMatch![0]!;
} else {
value = expectQuotedString(scanner);
}
scanner.scan(whitespace);
parameters[attribute] = value;
}
scanner.expectDone();
return MediaType(type, subtype, parameters);
});
MediaType(String type, String subtype, [Map<String, String>? parameters])
: type = type.toLowerCase(),
subtype = subtype.toLowerCase(),
parameters = UnmodifiableMapView(
parameters == null ? {} : CaseInsensitiveMap.from(parameters));
/// Returns a copy of this [MediaType] with some fields altered.
///
/// [type] and [subtype] alter the corresponding fields. [mimeType] is parsed
/// and alters both the [type] and [subtype] fields; it cannot be passed along
/// with [type] or [subtype].
///
/// [parameters] overwrites and adds to the corresponding field. If
/// [clearParameters] is passed, it replaces the corresponding field entirely
/// instead.
MediaType change(
{String? type,
String? subtype,
String? mimeType,
Map<String, String>? parameters,
bool clearParameters = false}) {
if (mimeType != null) {
if (type != null) {
throw ArgumentError('You may not pass both [type] and [mimeType].');
} else if (subtype != null) {
throw ArgumentError('You may not pass both [subtype] and '
'[mimeType].');
}
final segments = mimeType.split('/');
if (segments.length != 2) {
throw FormatException('Invalid mime type "$mimeType".');
}
type = segments[0];
subtype = segments[1];
}
type ??= this.type;
subtype ??= this.subtype;
parameters ??= {};
if (!clearParameters) {
final newParameters = parameters;
parameters = Map.from(this.parameters);
parameters.addAll(newParameters);
}
return MediaType(type, subtype, parameters);
}
/// Converts the media type to a string.
///
/// This will produce a valid HTTP media type.
@override
String toString() {
final buffer = StringBuffer()..write(type)..write('/')..write(subtype);
parameters.forEach((attribute, value) {
buffer.write('; $attribute=');
if (nonToken.hasMatch(value)) {
buffer
..write('"')
..write(
value.replaceAllMapped(_escapedChar, (match) => '\\${match[0]}'))
..write('"');
} else {
buffer.write(value);
}
});
return buffer.toString();
}
}