Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(940)

Side by Side Diff: lib/src/data_uri.dart

Issue 1390353008: Add a DataUri class. (Closed) Base URL: git@github.com:dart-lang/http_parser@master
Patch Set: merge Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2015, 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:convert';
6
7 import 'package:convert/convert.dart';
8 import 'package:crypto/crypto.dart';
9 import 'package:string_scanner/string_scanner.dart';
10
11 import 'media_type.dart';
12 import 'scan.dart';
13 import 'utils.dart';
14
15 /// Like [whitespace] from scan.dart, except that it matches URI-encoded
16 /// whitespace rather than literal characters.
17 final _whitespace = new RegExp(r'(?:(?:%0D%0A)?(?:%20|%09)+)*');
18
19 /// A converter for percent encoding strings using UTF-8.
20 final _utf8Percent = UTF8.fuse(percent);
21
22 /// A class representing a `data:` URI that provides access to its [mediaType]
23 /// and the [data] it contains.
24 ///
25 /// Data can be encoded as a `data:` URI using [encode] or [encodeString], and
26 /// decoded using [decode].
27 ///
28 /// This implementation is based on [RFC 2397][rfc], but as that RFC is
29 /// [notoriously ambiguous][ambiguities], some judgment calls have been made.
30 /// This class tries to match browsers' data URI logic, to ensure that it can
31 /// losslessly parse its own output, and to accept as much input as it can make
32 /// sense of. A balance has been struck between these goals so that while none
33 /// of them have been accomplished perfectly, all of them are close enough for
34 /// practical use.
35 ///
36 /// [rfc]: http://tools.ietf.org/html/rfc2397
37 /// [ambiguities]: https://simonsapin.github.io/data-urls/
38 ///
39 /// Some particular notes on the behavior:
40 ///
41 /// * When encoding, all characters that are not [reserved][] in the type,
42 /// subtype, parameter names, and parameter values of media types are
43 /// percent-encoded using UTF-8.
44 ///
45 /// * When decoding, the type, subtype, parameter names, and parameter values of
46 /// media types are percent-decoded using UTF-8. Parameter values are allowed
47 /// to contain non-token characters once decoded, but the other tokens are
48 /// not.
49 ///
50 /// * As per the spec, quoted-string parameters are not supported when decoding.
51 ///
52 /// * Query components are included in the decoding algorithm, but fragments are
53 /// not.
54 ///
55 /// * Invalid media types and parameters will raise exceptions when decoding.
56 /// This is standard for Dart parsers but contrary to browser behavior.
57 ///
58 /// * The URL and filename-safe base64 alphabet is accepted when decoding but
59 /// never emitted when encoding, since browsers don't support it.
60 ///
61 /// [lws]: https://tools.ietf.org/html/rfc2616#section-2.2
62 /// [reserved]: https://tools.ietf.org/html/rfc3986#section-2.2
63 class DataUri implements Uri {
64 /// The inner URI to which all [Uri] methods are forwarded.
65 final Uri _inner;
66
67 /// The byte data contained in the data URI.
68 final List<int> data;
69
70 /// The media type declared for the data URI.
71 ///
72 /// This defaults to `text/plain;charset=US-ASCII`.
73 final MediaType mediaType;
74
75 /// The encoding declared by the `charset` parameter in [mediaType].
76 ///
77 /// If [mediaType] has no `charset` parameter, this defaults to [ASCII]. If
78 /// the `charset` parameter declares an encoding that can't be found using
79 /// [Encoding.getByName], this returns `null`.
80 Encoding get declaredEncoding {
81 var charset = mediaType.parameters["charset"];
82 return charset == null ? ASCII : Encoding.getByName(charset);
83 }
84
85 /// Creates a new data URI with the given [mediaType] and [data].
86 ///
87 /// If [base64] is `true` (the default), the data is base64-encoded;
88 /// otherwise, it's percent-encoded.
89 ///
90 /// If [encoding] is passed or [mediaType] declares a `charset` parameter,
91 /// [data] is encoded using that encoding. Otherwise, it's encoded using
92 /// [UTF8] or [ASCII] depending on whether it contains any non-ASCII
93 /// characters.
94 ///
95 /// Throws [ArgumentError] if [mediaType] and [encoding] disagree on the
96 /// encoding, and an [UnsupportedError] if [mediaType] defines an encoding
97 /// that's not supported by [Encoding.getByName].
98 factory DataUri.encodeString(String data, {bool base64: true,
99 MediaType mediaType, Encoding encoding}) {
100 if (mediaType == null) mediaType = new MediaType("text", "plain");
101
102 var charset = mediaType.parameters["charset"];
103 var bytes;
104 if (encoding != null) {
105 if (charset == null) {
106 mediaType = mediaType.change(parameters: {"charset": encoding.name});
107 } else if (Encoding.getByName(charset) != encoding) {
108 throw new ArgumentError("Media type charset '$charset' disagrees with "
109 "encoding '${encoding.name}'.");
110 }
111 bytes = encoding.encode(data);
112 } else if (charset != null) {
113 encoding = Encoding.getByName(charset);
114 if (encoding == null) {
115 throw new UnsupportedError(
116 'Unsupported media type charset "$charset".');
117 }
118 bytes = encoding.encode(data);
119 } else if (data.codeUnits.every((codeUnit) => codeUnit < 0x80)) {
120 // If the data is pure ASCII, don't bother explicitly defining a charset.
121 bytes = data.codeUnits;
122 } else {
123 // If the data isn't pure ASCII, default to UTF-8.
124 bytes = UTF8.encode(data);
125 mediaType = mediaType.change(parameters: {"charset": "utf-8"});
126 }
127
128 return new DataUri.encode(bytes, base64: base64, mediaType: mediaType);
129 }
130
131 /// Creates a new data URI with the given [mediaType] and [data].
132 ///
133 /// If [base64] is `true` (the default), the data is base64-encoded;
134 /// otherwise, it's percent-encoded.
135 factory DataUri.encode(List<int> data, {bool base64: true,
136 MediaType mediaType}) {
137 mediaType ??= new MediaType('text', 'plain');
kevmoo 2015/10/19 22:45:21 requires at least dart 1.12, right?
nweiz 2015/10/19 23:08:32 Done.
138
139 var buffer = new StringBuffer();
140
141 // Manually stringify the media type because [section 3][rfc] requires that
142 // parameter values should have non-token characters URL-escaped rather than
143 // emitting them as quoted-strings. This also allows us to omit text/plain
144 // if possible.
145 //
146 // [rfc]: http://tools.ietf.org/html/rfc2397#section-3
147 if (mediaType.type != 'text' || mediaType.subtype != 'plain') {
148 buffer.write(_utf8Percent.encode(mediaType.type));
149 buffer.write("/");
150 buffer.write(_utf8Percent.encode(mediaType.subtype));
151 }
152
153 mediaType.parameters.forEach((attribute, value) {
154 buffer.write(";${_utf8Percent.encode(attribute)}=");
155 buffer.write(_utf8Percent.encode(value));
156 });
157
158 if (base64) {
159 buffer.write(";base64,");
160 // *Don't* use the URL-safe encoding scheme, since browsers don't actually
161 // support it.
162 buffer.write(CryptoUtils.bytesToBase64(data));
163 } else {
164 buffer.write(",");
165 buffer.write(percent.encode(data));
166 }
167
168 return new DataUri._(data, mediaType,
169 new Uri(scheme: 'data', path: buffer.toString()));
170 }
171
172 /// Decodes [uri] to make its [data] and [mediaType] available.
173 ///
174 /// [uri] may be a [Uri] or a [String].
175 ///
176 /// Throws an [ArgumentError] if [uri] is an invalid type or has a scheme
177 /// other than `data:`. Throws a [FormatException] if parsing fails.
178 factory DataUri.decode(uri) {
179 if (uri is String) {
180 uri = Uri.parse(uri);
181 } else if (uri is! Uri) {
182 throw new ArgumentError.value(uri, "uri", "Must be a String or a Uri.");
183 }
184
185 if (uri.scheme != 'data') {
186 throw new ArgumentError.value(uri, "uri", "Can only decode a data: URI.");
187 }
188
189 return wrapFormatException("data URI", uri.toString(), () {
190 // Remove the fragment, as per https://simonsapin.github.io/data-urls/.
191 // TODO(nweiz): Use Uri.removeFragment once sdk#24593 is fixed.
192 var string = uri.toString();
193 var fragment = string.indexOf('#');
194 if (fragment != -1) string = string.substring(0, fragment);
195 var scanner = new StringScanner(string);
196 scanner.expect('data:');
197
198 // Manually scan the media type for three reasons:
199 //
200 // * Media type parameter values that aren't valid tokens are URL-encoded
201 // rather than quoted.
202 //
203 // * The media type may be omitted without omitting the parameters.
204 //
205 // * We need to be able to stop once we reach `;base64,`, even though at
206 // first it looks like a parameter.
207 var type;
208 var subtype;
209 var implicitType = false;
210 if (scanner.scan(token)) {
211 type = _verifyToken(scanner);
212 scanner.expect('/');
213 subtype = _expectToken(scanner);
214 } else {
215 type = 'text';
216 subtype = 'plain';
217 implicitType = true;
218 }
219
220 // Scan the parameters, up through ";base64" or a comma.
221 var parameters = {};
222 var base64 = false;
223 while (scanner.scan(';')) {
224 var attribute = _expectToken(scanner);
225
226 if (attribute != 'base64') {
227 scanner.expect('=');
228 } else if (!scanner.scan('=')) {
229 base64 = true;
230 break;
231 }
232
233 // Don't use [_expectToken] because the value uses percent-encoding to
234 // escape non-token characters.
235 scanner.expect(token);
236 parameters[attribute] = _utf8Percent.decode(scanner.lastMatch[0]);
237 }
238 scanner.expect(',');
239
240 if (implicitType && parameters.isEmpty) {
241 parameters = {"charset": "US-ASCII"};
242 }
243
244 var mediaType = new MediaType(type, subtype, parameters);
245
246 var data = base64
247 ? CryptoUtils.base64StringToBytes(scanner.rest)
248 : percent.decode(scanner.rest);
249
250 return new DataUri._(data, mediaType, uri);
251 });
252 }
253
254 /// Returns the percent-decoded value of the last MIME token scanned by
255 /// [scanner].
256 ///
257 /// Throws a [FormatException] if it's not a valid token after
258 /// percent-decoding.
259 static String _verifyToken(StringScanner scanner) {
260 var value = _utf8Percent.decode(scanner.lastMatch[0]);
261 if (!value.contains(nonToken)) return value;
262 scanner.error("Invalid token.");
263 }
264
265 /// Scans [scanner] through a MIME token and returns its percent-decoded
266 /// value.
267 ///
268 /// Throws a [FormatException] if it's not a valid token after
269 /// percent-decoding.
270 static String _expectToken(StringScanner scanner) {
271 scanner.expect(token, name: "a token");
272 return _verifyToken(scanner);
273 }
274
275 DataUri._(this.data, this.mediaType, this._inner);
276
277 /// Returns the decoded [data] decoded using [encoding].
278 ///
279 /// [encoding] defaults to [declaredEncoding]. If the declared encoding isn't
280 /// supported by [Encoding.getByName] and [encoding] isn't passed, this throws
281 /// an [UnsupportedError].
282 String dataAsString({Encoding encoding}) {
283 encoding ??= declaredEncoding;
kevmoo 2015/10/19 22:45:21 ditto w/ 1.12 requirement
nweiz 2015/10/19 23:08:32 Done.
284 if (encoding == null) {
285 throw new UnsupportedError(
286 'Unsupported media type charset '
287 '"${mediaType.parameters["charset"]}".');
288 }
289
290 return encoding.decode(data);
291 }
292
293 String get scheme => _inner.scheme;
294 String get authority => _inner.authority;
295 String get userInfo => _inner.userInfo;
296 String get host => _inner.host;
297 int get port => _inner.port;
298 String get path => _inner.path;
299 String get query => _inner.query;
300 String get fragment => _inner.fragment;
301 Uri replace({String scheme, String userInfo, String host, int port,
302 String path, Iterable<String> pathSegments, String query,
303 Map<String, String> queryParameters, String fragment}) =>
304 _inner.replace(
305 scheme: scheme, userInfo: userInfo, host: host, port: port,
306 path: path, pathSegments: pathSegments, query: query,
307 queryParameters: queryParameters, fragment: fragment);
308 Uri removeFragment() => _inner.removeFragment();
309 List<String> get pathSegments => _inner.pathSegments;
310 Map<String, String> get queryParameters => _inner.queryParameters;
311 Uri normalizePath() => _inner.normalizePath();
312 bool get isAbsolute => _inner.isAbsolute;
313 Uri resolve(String reference) => _inner.resolve(reference);
314 Uri resolveUri(Uri reference) => _inner.resolveUri(reference);
315 bool get hasScheme => _inner.hasScheme;
316 bool get hasAuthority => _inner.hasAuthority;
317 bool get hasPort => _inner.hasPort;
318 bool get hasQuery => _inner.hasQuery;
319 bool get hasFragment => _inner.hasFragment;
320 bool get hasEmptyPath => _inner.hasEmptyPath;
321 bool get hasAbsolutePath => _inner.hasAbsolutePath;
322 String get origin => _inner.origin;
323 String toFilePath({bool windows}) => _inner.toFilePath(windows: windows);
324 String toString() => _inner.toString();
325 bool operator==(other) => _inner == other;
326 int get hashCode => _inner.hashCode;
327 }
OLDNEW
« no previous file with comments | « lib/http_parser.dart ('k') | lib/src/scan.dart » ('j') | lib/src/scan.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698