blob: fa9350e32eabdf8a2bcccf6a3a579fc357d8fd19 [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.
library http_parser.media_type;
import 'package:collection/collection.dart';
import 'package:string_scanner/string_scanner.dart';
// All of the following regular expressions come from section 2.2 of the HTTP
// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
final _lws = new RegExp(r"(?:\r\n)?[ \t]+");
final _token = new RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+');
final _quotedString = new RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"');
final _quotedPair = new RegExp(r'\\(.)');
/// A regular expression matching any number of [_lws] productions in a row.
final _whitespace = new RegExp("(?:${_lws.pattern})*");
/// A regular expression matching a character that is not a valid HTTP token.
final _nonToken = new RegExp(r'[()<>@,;:"\\/\[\]?={} \t\x00-\x1F\x7F]');
/// A regular expression matching a character that needs to be backslash-escaped
/// in a quoted string.
final _escapedChar = new 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.
final String type;
/// The secondary identifier of the MIME type.
final String subtype;
/// The parameters to the media type.
///
/// This map is immutable.
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.
try {
var scanner = new StringScanner(mediaType);
scanner.scan(_whitespace);
scanner.expect(_token);
var type = scanner.lastMatch[0];
scanner.expect('/');
scanner.expect(_token);
var subtype = scanner.lastMatch[0];
scanner.scan(_whitespace);
var parameters = {};
while (scanner.scan(';')) {
scanner.scan(_whitespace);
scanner.expect(_token);
var attribute = scanner.lastMatch[0];
scanner.expect('=');
var value;
if (scanner.scan(_token)) {
value = scanner.lastMatch[0];
} else {
scanner.expect(_quotedString);
var quotedString = scanner.lastMatch[0];
value = quotedString
.substring(1, quotedString.length - 1)
.replaceAllMapped(_quotedPair, (match) => match[1]);
}
scanner.scan(_whitespace);
parameters[attribute] = value;
}
scanner.expectDone();
return new MediaType(type, subtype, parameters);
} on FormatException catch (error) {
throw new FormatException(
'Invalid media type "$mediaType": ${error.message}');
}
}
MediaType(this.type, this.subtype, [Map<String, String> parameters])
: this.parameters = new UnmodifiableMapView(
parameters == null ? {} : new Map.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 new ArgumentError("You may not pass both [type] and [mimeType].");
} else if (subtype != null) {
throw new ArgumentError("You may not pass both [subtype] and "
"[mimeType].");
}
var segments = mimeType.split('/');
if (segments.length != 2) {
throw new FormatException('Invalid mime type "$mimeType".');
}
type = segments[0];
subtype = segments[1];
}
if (type == null) type = this.type;
if (subtype == null) subtype = this.subtype;
if (parameters == null) parameters = {};
if (!clearParameters) {
var newParameters = parameters;
parameters = new Map.from(this.parameters);
parameters.addAll(newParameters);
}
return new MediaType(type, subtype, parameters);
}
/// Converts the media type to a string.
///
/// This will produce a valid HTTP media type.
String toString() {
var buffer = new 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();
}
}