OLD | NEW |
---|---|
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 part of dart.core; | 5 part of dart.core; |
6 | 6 |
7 /** | 7 /** |
8 * A parsed URI, such as a URL. | 8 * A parsed URI, such as a URL. |
9 * | 9 * |
10 * **See also:** | 10 * **See also:** |
(...skipping 2291 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2302 * | 2302 * |
2303 * This function is similar to the JavaScript-function `decodeURI`. | 2303 * This function is similar to the JavaScript-function `decodeURI`. |
2304 * | 2304 * |
2305 * If [plusToSpace] is `true`, plus characters will be converted to spaces. | 2305 * If [plusToSpace] is `true`, plus characters will be converted to spaces. |
2306 * | 2306 * |
2307 * The decoder will create a byte-list of the percent-encoded parts, and then | 2307 * The decoder will create a byte-list of the percent-encoded parts, and then |
2308 * decode the byte-list using [encoding]. The default encodingis UTF-8. | 2308 * decode the byte-list using [encoding]. The default encodingis UTF-8. |
2309 */ | 2309 */ |
2310 static String _uriDecode(String text, | 2310 static String _uriDecode(String text, |
2311 {bool plusToSpace: false, | 2311 {bool plusToSpace: false, |
2312 Encoding encoding: UTF8}) { | 2312 Encoding encoding: UTF8, |
2313 int start: 0, | |
2314 int end}) { | |
2315 if (end == null) end = text.length; | |
floitsch
2015/11/07 02:56:26
if we have start and end, shouldn't we at least ch
Lasse Reichstein Nielsen
2015/11/09 10:27:38
This is a private function, I usually don't check
| |
2313 // First check whether there is any characters which need special handling. | 2316 // First check whether there is any characters which need special handling. |
2314 bool simple = true; | 2317 bool simple = true; |
2315 for (int i = 0; i < text.length && simple; i++) { | 2318 for (int i = start; i < end && simple; i++) { |
2316 var codeUnit = text.codeUnitAt(i); | 2319 var codeUnit = text.codeUnitAt(i); |
2317 simple = codeUnit != _PERCENT && codeUnit != _PLUS; | 2320 simple = codeUnit != _PERCENT && codeUnit != _PLUS; |
floitsch
2015/11/07 02:56:26
I prefer:
if (codeUnit == _PERCENT || codeUnit ==
Lasse Reichstein Nielsen
2015/11/09 10:27:38
Done.
| |
2318 } | 2321 } |
2319 List<int> bytes; | 2322 List<int> bytes; |
2320 if (simple) { | 2323 if (simple) { |
2321 if (encoding == UTF8 || encoding == LATIN1) { | 2324 if (encoding == UTF8 || encoding == LATIN1) { |
2322 return text; | 2325 return text.substring(start, end); |
2326 } else if (start == 0 && end == text.length) { | |
2327 bytes = text.codeUnits; | |
2323 } else { | 2328 } else { |
2324 bytes = text.codeUnits; | 2329 var decoder = encoding.decoder; |
2330 var result; | |
2331 var conversionSink = decoder.startChunkedConversion( | |
2332 new ChunkedConversionSink((list) { | |
2333 result = list.join(); | |
2334 })); | |
2335 if (conversionSink is ByteConversionSink) { | |
2336 conversionSink.addSlice(text.codeUnits, start, end, true); | |
2337 } else { | |
2338 conversionSink.add(text.codeUnits.sublist(start, end)); | |
2339 conversionSink.close(); | |
2340 } | |
2341 return result; | |
2325 } | 2342 } |
2326 } else { | 2343 } else { |
2327 bytes = new List(); | 2344 bytes = new List(); |
2328 for (int i = 0; i < text.length; i++) { | 2345 for (int i = start; i < end; i++) { |
2329 var codeUnit = text.codeUnitAt(i); | 2346 var codeUnit = text.codeUnitAt(i); |
2330 if (codeUnit > 127) { | 2347 if (codeUnit > 127) { |
2331 throw new ArgumentError("Illegal percent encoding in URI"); | 2348 throw new ArgumentError("Illegal percent encoding in URI"); |
2332 } | 2349 } |
2333 if (codeUnit == _PERCENT) { | 2350 if (codeUnit == _PERCENT) { |
2334 if (i + 3 > text.length) { | 2351 if (i + 3 > text.length) { |
2335 throw new ArgumentError('Truncated URI'); | 2352 throw new ArgumentError('Truncated URI'); |
2336 } | 2353 } |
2337 bytes.add(_hexCharPairToByte(text, i + 1)); | 2354 bytes.add(_hexCharPairToByte(text, i + 1)); |
2338 i += 2; | 2355 i += 2; |
(...skipping 251 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
2590 // 0123456789:; = ? | 2607 // 0123456789:; = ? |
2591 0xafff, // 0x30 - 0x3f 1111111111110101 | 2608 0xafff, // 0x30 - 0x3f 1111111111110101 |
2592 // @ABCDEFGHIJKLMNO | 2609 // @ABCDEFGHIJKLMNO |
2593 0xffff, // 0x40 - 0x4f 1111111111111111 | 2610 0xffff, // 0x40 - 0x4f 1111111111111111 |
2594 // PQRSTUVWXYZ _ | 2611 // PQRSTUVWXYZ _ |
2595 0x87ff, // 0x50 - 0x5f 1111111111100001 | 2612 0x87ff, // 0x50 - 0x5f 1111111111100001 |
2596 // abcdefghijklmno | 2613 // abcdefghijklmno |
2597 0xfffe, // 0x60 - 0x6f 0111111111111111 | 2614 0xfffe, // 0x60 - 0x6f 0111111111111111 |
2598 // pqrstuvwxyz ~ | 2615 // pqrstuvwxyz ~ |
2599 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 2616 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
2617 | |
2600 } | 2618 } |
2619 | |
2620 // -------------------------------------------------------------------- | |
2621 // Data URI | |
2622 // -------------------------------------------------------------------- | |
2623 | |
2624 /** | |
2625 * A representation of a `data:` URI. | |
2626 * | |
2627 * Data URIs are non-hierarchial URIs that contain can contain any data. | |
2628 * They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397). | |
2629 * | |
2630 * This class allows parsing the URI text and extracting individual parts of the | |
2631 * URI, as well as building the URI text from structured parts. | |
2632 */ | |
2633 class DataUri { | |
nweiz
2015/10/15 21:09:03
It seems strange that this isn't a subclass of Uri
Lasse Reichstein Nielsen
2015/10/28 13:55:47
The Uri class is designed only for hierarchial URI
nweiz
2015/10/29 00:28:36
That's not communicated anywhere in the documentat
Lasse Reichstein Nielsen
2015/11/03 18:02:52
Good points.
So this class is not really a "data U
Lasse Reichstein Nielsen
2015/11/09 10:27:37
On 2015/11/03 18:02:52, Lasse Reichstein Nielsen w
| |
2634 static const int _noScheme = -1; | |
2635 /** | |
2636 * Contains the text content of a `data:` URI, with or without a | |
2637 * leading `data:`. | |
2638 * | |
2639 * If [_separatorIndices] starts with `4` (the index of the `:`), then | |
2640 * there is a leading `data:`, otherwise _separatorIndices starts with | |
2641 * `-1`. | |
2642 */ | |
2643 final String _text; | |
2644 | |
2645 /** | |
2646 * List of the separators (';', '=' and ',') in the text. | |
2647 * | |
2648 * Starts with the index of the index of the `:` in `data:` of the mimeType. | |
2649 * That is always either -1 or 4, depending on whether `_text` includes the | |
2650 * `data:` scheme or not. | |
2651 * | |
2652 * The first speparator ends the mime type. We don't bother with finding | |
2653 * the '/' inside the mime type. | |
2654 * | |
2655 * Each two separators after that marks a parameter key and value. | |
2656 * | |
2657 * If there is a single separator left, it ends the "base64" marker. | |
2658 * | |
2659 * So the following separators are found for a text: | |
2660 * | |
2661 * data:text/plain;foo=bar;base64,ARGLEBARGLE= | |
2662 * ^ ^ ^ ^ ^ | |
2663 * | |
2664 */ | |
2665 List<int> _separatorIndices; | |
2666 | |
2667 DataUri._(this._text, | |
2668 this._separatorIndices); | |
2669 | |
2670 /** The entire content of the data URI, including the leading `data:`. */ | |
2671 String get text => _separatorIndices[0] == _noScheme ? "data:$_text" : _text; | |
2672 | |
2673 /** | |
2674 * Creates a `data:` URI containing the contents as percent-encoded text. | |
2675 */ | |
2676 factory DataUri.fromString(String content, | |
nweiz
2015/10/15 21:09:03
Why can't you base64-encode text, or percent-encod
Lasse Reichstein Nielsen
2015/10/16 14:38:45
You can't base-64 encode text - base64 encoding on
nweiz
2015/10/19 19:51:20
I was thinking something parallel to File.writeAsS
Lasse Reichstein Nielsen
2015/10/28 13:55:47
True. We probably should have an encoding here: Th
| |
2677 {mimeType: "text/plain", | |
2678 Iterable<DataUriParameter> parameters}) { | |
nweiz
2015/10/15 21:09:03
Right now, the encoding is implicitly always UTF-8
Lasse Reichstein Nielsen
2015/10/16 14:38:45
Good point. The default, if nothing is written, is
Lasse Reichstein Nielsen
2015/10/28 13:55:47
I've changed this to add an "Encoding charset" par
| |
2679 StringBuffer buffer = new StringBuffer(); | |
2680 List indices = [_noScheme]; | |
2681 _writeUri(mimeType, parameters, buffer, indices); | |
2682 indices.add(buffer.length); | |
2683 buffer.write(','); | |
2684 buffer.write(Uri.encodeComponent(content)); | |
nweiz
2015/10/15 21:09:04
URI.encodeComponent doesn't encode a number of res
Lasse Reichstein Nielsen
2015/10/16 14:38:45
ACK. The syntax for parameter keys an values are R
nweiz
2015/10/19 19:51:20
You should certainly percent-encode *here*, becaus
Lasse Reichstein Nielsen
2015/10/28 13:55:47
Ack, rereading the RFC again. And again.
It's almo
nweiz
2015/10/29 00:28:36
I think that's the right behavior. I believe there
| |
2685 return new DataUri._(buffer.toString(), indices); | |
2686 } | |
2687 | |
2688 /** | |
2689 * Creates a `data:` URI string containing the base-64 encoded content bytes. | |
2690 * | |
2691 * It defaults to having the mime-type `application/octet-stream`. | |
2692 */ | |
2693 factory DataUri.fromBytes(List<int> bytes, | |
2694 {mimeType: "application/octet-stream", | |
2695 Iterable<DataUriParameter> parameters}) { | |
2696 StringBuffer buffer = new StringBuffer(); | |
2697 List indices = [_noScheme]; | |
2698 _writeUri(mimeType, parameters, buffer, indices); | |
2699 indices.add(buffer.length); | |
2700 buffer.write(';base64,'); | |
2701 indices.add(buffer.length - 1); | |
2702 BASE64.encoder.startChunkedConversion( | |
2703 new StringConversionSink.fromStringSink(buffer)) | |
2704 .addSlice(bytes, 0, bytes.length, true); | |
2705 return new DataUri._(buffer.toString(), indices); | |
2706 } | |
2707 | |
2708 /** | |
2709 * Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme]. | |
2710 * | |
2711 * The [uri] must have scheme `data` and no authority, query or fragment, | |
2712 * and the path must be valid as a data URI. | |
2713 */ | |
2714 factory DataUri.fromUri(Uri uri) { | |
nweiz
2015/10/15 21:09:03
This should document when it will throw FormatExce
Lasse Reichstein Nielsen
2015/10/16 14:38:45
Good point.
Lasse Reichstein Nielsen
2015/10/28 13:55:47
Documented on parse, referenced here.
| |
2715 if (uri.scheme != "data") { | |
2716 throw new ArgumentError.value(uri, "uri", | |
2717 "Scheme must be 'data'"); | |
2718 } | |
2719 if (uri.hasAuthority) { | |
2720 throw new ArgumentError.value(uri, "uri", | |
2721 "Data uri must not have authority"); | |
2722 } | |
2723 if (uri.hasQuery) { | |
2724 throw new ArgumentError.value(uri, "uri", | |
2725 "Data uri must not have a query part"); | |
2726 } | |
2727 if (uri.hasFragment) { | |
2728 throw new ArgumentError.value(uri, "uri", | |
2729 "Data uri must not have a fragment part"); | |
2730 } | |
nweiz
2015/10/15 21:09:03
According to https://simonsapin.github.io/data-url
Lasse Reichstein Nielsen
2015/10/16 14:38:45
True - '?' is a valid uric, so the query should be
Lasse Reichstein Nielsen
2015/10/28 13:55:47
True. Fixed.
| |
2731 return _parse(uri.path, 0); | |
2732 } | |
2733 | |
2734 /** | |
2735 * Writes the initial part of a `data:` uri, from after the "data:" | |
2736 * until just before the ',' before the data, or before a `;base64,` | |
2737 * marker. | |
2738 * | |
2739 * Of an [indices] list is passed, separator indices are stored in that | |
2740 * list. | |
2741 */ | |
2742 static void _writeUri(String mimeType, | |
2743 Iterable<DataUriParameter> parameters, | |
2744 StringBuffer buffer, List indices) { | |
2745 if (mimeType == null) { | |
2746 mimeType = "text/plain"; | |
2747 } | |
2748 if (mimeType.isEmpty || | |
2749 identical(mimeType, "text/plain") || | |
nweiz
2015/10/15 21:09:03
Consider omitting the text/plain mime type, since
Lasse Reichstein Nielsen
2015/10/16 14:38:45
Good idea.
| |
2750 identical(mimeType, "application/octet-stream")) { | |
2751 buffer.write(mimeType); // Common cases need no escaping. | |
2752 } else { | |
2753 int slashIndex = _validateMimeType(mimeType); | |
2754 if (slashIndex < 0) { | |
2755 throw new ArgumentError.value(mimeType, "mimeType", | |
2756 "Invalid MIME type"); | |
2757 } | |
2758 buffer.write(Uri._uriEncode(_tokenCharTable, | |
2759 mimeType.substring(0, slashIndex))); | |
2760 buffer.write("/"); | |
2761 buffer.write(Uri._uriEncode(_tokenCharTable, | |
2762 mimeType.substring(slashIndex + 1))); | |
2763 } | |
2764 if (parameters != null) { | |
2765 for (var parameter in parameters) { | |
2766 if (indices != null) indices.add(buffer.length); | |
2767 buffer.write(';'); | |
2768 // Encode any non-RFC2045-token character as well as '%' and '#'. | |
2769 buffer.write(Uri._uriEncode(_tokenCharTable, parameter.key)); | |
2770 if (indices != null) indices.add(buffer.length); | |
2771 buffer.write('='); | |
2772 buffer.write(Uri._uriEncode(_tokenCharTable, parameter.value)); | |
2773 } | |
2774 } | |
2775 } | |
2776 | |
2777 /** | |
2778 * Checks mimeType is valid-ish (`token '/' token`). | |
2779 * | |
2780 * Returns the index of the slash, or -1 if the mime type is not | |
2781 * considered valid. | |
2782 * | |
2783 * Currently only looks for slashes, all other characters will be | |
2784 * percent-encoded as UTF-8 if necessary. | |
2785 */ | |
2786 static int _validateMimeType(String mimeType) { | |
2787 int slashIndex = -1; | |
2788 for (int i = 0; i < mimeType.length; i++) { | |
2789 var char = mimeType.codeUnitAt(i); | |
2790 if (char != Uri._SLASH) continue; | |
2791 if (slashIndex < 0) { | |
2792 slashIndex = i; | |
2793 continue; | |
2794 } | |
2795 return -1; | |
2796 } | |
2797 return slashIndex; | |
2798 } | |
2799 | |
2800 /** | |
2801 * Creates a [Uri] with the content of [DataUri.fromString]. | |
2802 * | |
2803 * The resulting URI will have `data` as scheme and the remainder | |
2804 * of the data URI as path. | |
2805 * | |
2806 * Equivalent to creating a `DataUri` using `new DataUri.fromString` and | |
2807 * calling `toUri` on the result. | |
2808 */ | |
2809 static Uri uriFromString(String content, | |
2810 {mimeType: "text/plain", | |
2811 Iterable<DataUriParameter> parameters}) { | |
2812 var buffer = new StringBuffer(); | |
2813 _writeUri(mimeType, parameters, buffer, null); | |
2814 buffer.write(','); | |
2815 buffer.write(Uri.encodeComponent(content)); | |
2816 return new Uri(scheme: "data", path: buffer.toString()); | |
2817 } | |
2818 | |
2819 /** | |
2820 * Creates a [Uri] with the content of [DataUri.fromBytes]. | |
2821 * | |
2822 * The resulting URI will have `data` as scheme and the remainder | |
2823 * of the data URI as path. | |
2824 * | |
2825 * Equivalent to creating a `DataUri` using `new DataUri.fromBytes` and | |
2826 * calling `toUri` on the result. | |
2827 */ | |
2828 static Uri uriFromBytes(List<int> bytes, | |
2829 {mimeType: "text/plain", | |
2830 Iterable<DataUriParameter> parameters}) { | |
2831 var buffer = new StringBuffer(); | |
2832 _writeUri(mimeType, parameters, buffer, null); | |
2833 buffer.write(';base64,'); | |
2834 BASE64.encoder.startChunkedConversion(buffer) | |
2835 .addSlice(bytes, 0, bytes.length, true); | |
2836 return new Uri(scheme: "data", path: buffer.toString()); | |
2837 } | |
2838 | |
2839 /** | |
2840 * Parses a string as a `data` URI. | |
2841 */ | |
2842 static DataUri parse(String uri) { | |
2843 if (!uri.startsWith("data:")) { | |
2844 throw new FormatException("Does not start with 'data:'", uri, 0); | |
2845 } | |
2846 return _parse(uri, 5); | |
2847 } | |
2848 | |
2849 /** | |
2850 * Converts a `DataUri` to a [Uri]. | |
2851 * | |
2852 * Returns a `Uri` with scheme `data` and the remainder of the data URI | |
2853 * as path. | |
2854 */ | |
2855 Uri toUri() { | |
2856 String content = _text; | |
2857 int colonIndex = _separatorIndices[0]; | |
2858 if (colonIndex >= 0) { | |
2859 content = _text.substring(colonIndex + 1); | |
2860 } | |
2861 return new Uri._internal("data", null, null, null, content, null, null); | |
2862 } | |
2863 | |
2864 /** | |
2865 * The MIME type of the data URI. | |
2866 * | |
2867 * A data URI consists of a "media type" followed by data. | |
2868 * The mediatype starts with a MIME type and can be followed by | |
2869 * extra parameters. | |
2870 * | |
2871 * Example: | |
2872 * | |
2873 * data:text/plain;encoding=utf-8,Hello%20World! | |
2874 * | |
2875 * This data URI has the media type `text/plain;encoding=utf-8`, which is the | |
2876 * MIME type `text/plain` with the parameter `encoding` with value `utf-8`. | |
2877 * See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail. | |
2878 * | |
2879 * If the first part of the data URI is empty, it defaults to `text/plain`. | |
2880 */ | |
2881 String get mimeType { | |
2882 int start = _separatorIndices[0] + 1; | |
2883 int end = _separatorIndices[1]; | |
2884 if (start == end) return "text/plain"; | |
2885 return Uri._uriDecode(_text, start: start, end: end); | |
2886 } | |
2887 | |
2888 /** | |
2889 * Whether the data is base64 encoded or not. | |
2890 */ | |
2891 bool get isBase64 => _separatorIndices.length.isOdd; | |
2892 | |
2893 /** | |
2894 * The content part of the data URI, as its actual representation. | |
2895 * | |
2896 * This string may contain percent escapes. | |
2897 */ | |
2898 String get contentText => _text.substring(_separatorIndices.last + 1); | |
2899 | |
2900 /** | |
2901 * The content part of the data URI as bytes. | |
2902 * | |
2903 * If the data is base64 encoded, it will be decoded to bytes. | |
2904 * | |
2905 * If the data is not base64 encoded, it will be decoded by unescaping | |
2906 * percent-escaped characters and returning byte values of each unescaped | |
2907 * character. The bytes will not be, e.g., UTF-8 decoded. | |
2908 */ | |
2909 List<int> contentAsBytes() { | |
2910 String text = _text; | |
2911 int start = _separatorIndices.last + 1; | |
2912 if (isBase64) { | |
2913 if (text.endsWith("%3D")) { | |
2914 return BASE64.decode(Uri._uriDecode(text, start: start, | |
2915 encoding: LATIN1)); | |
nweiz
2015/10/15 21:09:04
Why does this assume a LATIN1 encoding? It should
Lasse Reichstein Nielsen
2015/10/16 14:38:45
This function is not creating text, only bytes, so
nweiz
2015/10/19 19:51:20
It would be good to document that in a comment.
Lasse Reichstein Nielsen
2015/10/28 13:55:47
No longer necessary using the new BASE64 decoder
(
| |
2916 } | |
2917 return BASE64.decode(text.substring(start)); | |
2918 } | |
2919 | |
2920 // Not base64, do percent-decoding and return the remaining bytes. | |
2921 // Compute result size. | |
2922 const int percent = 0x25; | |
2923 int length = text.length - start; | |
2924 for (int i = start; i < text.length; i++) { | |
2925 var codeUnit = text.codeUnitAt(i); | |
2926 if (codeUnit == percent) { | |
2927 i += 2; | |
2928 length -= 2; | |
2929 } | |
2930 } | |
2931 // Fill result array. | |
2932 Uint8List result = new Uint8List(length); | |
2933 if (length == text.length) { | |
2934 result.setRange(0, length, text.codeUnits, start); | |
2935 return result; | |
2936 } | |
2937 int index = 0; | |
2938 for (int i = start; i < text.length; i++) { | |
2939 var codeUnit = text.codeUnitAt(i); | |
2940 if (codeUnit != percent) { | |
2941 result[index++] = codeUnit; | |
2942 } else { | |
2943 if (i + 2 < text.length) { | |
2944 var digit1 = _hexDigit(text.codeUnitAt(i + 1)); | |
2945 var digit2 = _hexDigit(text.codeUnitAt(i + 2)); | |
2946 if (digit1 >= 0 && digit2 >= 0) { | |
2947 int byte = digit1 * 16 + digit2; | |
2948 result[index++] = byte; | |
2949 i += 2; | |
2950 continue; | |
2951 } | |
2952 } | |
2953 throw new FormatException("Invalid percent escape", text, i); | |
2954 } | |
2955 } | |
2956 assert(index == result.length); | |
2957 return result; | |
2958 } | |
2959 | |
2960 // Converts a UTF-16 code-unit to its value as a hex digit. | |
2961 // Returns -1 for non-hex digits. | |
2962 int _hexDigit(int char) { | |
2963 const int char_0 = 0x30; | |
2964 const int char_a = 0x61; | |
2965 | |
2966 int digit = char ^ char_0; | |
2967 if (digit <= 9) return digit; | |
2968 char = ((char | 0x20) - char_a) & 0xFFFF; | |
2969 if (char < 6) return 10 + char; | |
2970 return -1; | |
2971 } | |
2972 | |
2973 /** | |
2974 * Returns a string created from the content of the data URI. | |
2975 * | |
2976 * If the content is base64 encoded, it will be decoded to bytes and then | |
2977 * decoded to a string using [encoding]. | |
2978 * | |
2979 * If the content is not base64 encoded, it will first have percent-escapes | |
2980 * converted to bytes and then the character codes and byte values are | |
2981 * decoded using [encoding]. | |
2982 */ | |
2983 String contentAsString({Encoding encoding: UTF8}) { | |
nweiz
2015/10/15 21:09:03
The encoding should be taken from the URI's parame
Lasse Reichstein Nielsen
2015/10/16 14:38:45
I really, really don't want to parse the "charset"
nweiz
2015/10/19 19:51:20
I think never failing by default is less useful th
Lasse Reichstein Nielsen
2015/10/28 13:55:47
Sounds reasonable. If there is no charset paramete
| |
2984 String text = _text; | |
2985 int start = _separatorIndices.last + 1; | |
2986 if (isBase64) { | |
2987 var converter = BASE64.decoder.fuse(encoding.decoder); | |
2988 if (text.endsWith("%3D")) { | |
2989 return converter.convert(Uri._uriDecode(text, start: start, | |
2990 encoding: LATIN1)); | |
2991 } | |
2992 return converter.convert(text.substring(start)); | |
2993 } | |
2994 return Uri._uriDecode(text, start: start, encoding: encoding); | |
2995 } | |
2996 | |
2997 /** | |
2998 * An iterable over the parameters of the data URI. | |
2999 * | |
3000 * A data URI may contain parameters between the the MIMI type and the | |
nweiz
2015/10/15 21:09:03
Nit: "MIMI" -> "MIME"
Lasse Reichstein Nielsen
2015/10/16 14:38:45
:)
| |
3001 * data. This iterates through those parameters, returning each as a | |
3002 * [DataUriParameter] pair of key and value. | |
3003 */ | |
3004 Iterable<DataUriParameter> get parameters sync* { | |
3005 for (int i = 3; i < _separatorIndices.length; i += 2) { | |
3006 var start = _separatorIndices[i - 2] + 1; | |
3007 var equals = _separatorIndices[i - 1]; | |
3008 var end = _separatorIndices[i]; | |
nweiz
2015/10/15 21:09:04
It looks like this will incorrectly accept invalid
Lasse Reichstein Nielsen
2015/10/16 14:38:45
The parser should avoid that. This assumes that th
nweiz
2015/10/19 19:51:20
That might be true. It's a good thing to write tes
Lasse Reichstein Nielsen
2015/10/28 13:55:47
It's not even possible to test it, because it's al
| |
3009 String key = Uri._uriDecode(_text, start: start, end: equals); | |
3010 String value = Uri._uriDecode(_text, start: equals + 1, end: end); | |
nweiz
2015/10/15 21:09:03
What about whitespace? If the spec is interpreted
Lasse Reichstein Nielsen
2015/10/16 14:38:45
The URI grammars don't generally allow space chara
nweiz
2015/10/19 19:51:20
Chrome accepts literal spaces in data URIs, but no
Lasse Reichstein Nielsen
2015/10/28 13:55:47
ACK, so we must escape existing percent characters
Lasse Reichstein Nielsen
2015/10/28 13:55:47
It seems Chrome distinguishes between "data:text/h
| |
3011 yield new DataUriParameter(key, value); | |
3012 } | |
3013 } | |
3014 | |
3015 static DataUri _parse(String text, int start) { | |
3016 assert(start == 0 || start == 5); | |
3017 assert((start == 5) == text.startsWith("data:")); | |
3018 | |
3019 /// Character codes. | |
3020 const int comma = 0x2c; | |
3021 const int slash = 0x2f; | |
3022 const int semicolon = 0x3b; | |
3023 const int equals = 0x3d; | |
3024 List indices = [start - 1]; | |
3025 int slashIndex = -1; | |
3026 var char; | |
3027 int i = start; | |
3028 for (; i < text.length; i++) { | |
3029 char = text.codeUnitAt(i); | |
3030 if (char == comma || char == semicolon) break; | |
3031 if (char == slash) { | |
3032 if (slashIndex < 0) { | |
3033 slashIndex = i; | |
3034 continue; | |
3035 } | |
3036 throw new FormatException("Invalid MIME type", text, i); | |
3037 } | |
3038 } | |
3039 if (slashIndex < 0 && i > start) { | |
3040 // An empty MIME type is allowed, but if non-empty it must contain | |
3041 // exactly one slash. | |
3042 throw new FormatException("Invalid MIME type", text, i); | |
3043 } | |
3044 while (char != comma) { | |
3045 // parse parameters and/or "base64". | |
3046 indices.add(i); | |
3047 i++; | |
3048 int equalsIndex = -1; | |
3049 for (; i < text.length; i++) { | |
3050 char = text.codeUnitAt(i); | |
3051 if (char == equals) { | |
3052 if (equalsIndex < 0) equalsIndex = i; | |
3053 } else if (char == semicolon || char == comma) { | |
3054 break; | |
3055 } | |
3056 } | |
3057 if (equalsIndex >= 0) { | |
3058 indices.add(equalsIndex); | |
3059 } else { | |
3060 // Have to be final "base64". | |
3061 var lastSeparator = indices.last; | |
3062 if (char != comma || | |
3063 i != lastSeparator + 7 /* "base64,".length */ || | |
3064 !text.startsWith("base64", lastSeparator + 1)) { | |
3065 throw new FormatException("Expecting '='", text, i); | |
3066 } | |
3067 break; | |
3068 } | |
3069 } | |
3070 indices.add(i); | |
3071 return new DataUri._(text, indices); | |
3072 } | |
3073 | |
3074 String toString() => text; | |
3075 | |
3076 // Table of the `token` characters of RFC 2045 in a URI. | |
3077 // | |
3078 // A token is any US-ASCII character except SPACE, control characters and | |
3079 // `tspecial` characters. The `tspecial` category is: | |
3080 // '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='. | |
3081 // | |
3082 // In a data URI, we also need to escape '%' and '#' characters. | |
3083 static const _tokenCharTable = const [ | |
3084 // LSB MSB | |
3085 // | | | |
3086 0x0000, // 0x00 - 0x0f 00000000 00000000 | |
3087 0x0000, // 0x10 - 0x1f 00000000 00000000 | |
3088 // ! $ &' *+ -. | |
3089 0x6cd2, // 0x20 - 0x2f 01001011 00110110 | |
3090 // 01234567 89 | |
3091 0x03ff, // 0x30 - 0x3f 11111111 11000000 | |
3092 // ABCDEFG HIJKLMNO | |
3093 0xfffe, // 0x40 - 0x4f 01111111 11111111 | |
3094 // PQRSTUVW XYZ ^_ | |
3095 0xc7ff, // 0x50 - 0x5f 11111111 11100011 | |
3096 // `abcdefg hijklmno | |
3097 0xffff, // 0x60 - 0x6f 11111111 11111111 | |
3098 // pqrstuvw xyz{|}~ | |
3099 0x7fff]; // 0x70 - 0x7f 11111111 11111110 | |
3100 } | |
3101 | |
3102 /** | |
3103 * A parameter of a data URI. | |
3104 * | |
3105 * A parameter is a key and a value. | |
3106 * | |
3107 * The key and value are the actual values to be encoded into the URI. | |
3108 * They will be escaped if necessary when creating a data URI, | |
3109 * and have been unescaped when extracted from a data URI. | |
3110 */ | |
3111 class DataUriParameter { | |
nweiz
2015/10/15 21:09:03
Why isn't this just a map? Maps are much easier to
Lasse Reichstein Nielsen
2015/10/16 14:38:45
Because the same parameter name may occur more tha
nweiz
2015/10/19 19:51:20
Can they? The MIME spec isn't explicit about this,
Lasse Reichstein Nielsen
2015/10/28 13:55:47
Good point. It doesn't actually look like paramete
| |
3112 /** Parameter key. */ | |
3113 final String key; | |
3114 /** Parameter value. */ | |
3115 final String value; | |
3116 DataUriParameter(this.key, this.value); | |
3117 | |
3118 /** | |
3119 * Creates an iterable of parameters from a map from key to value. | |
3120 * | |
3121 * Parameter keys are not required to be unique in a data URI, but | |
3122 * when they are, a map can be used to represent the parameters, and | |
3123 * this function provides a way to access the map pairs as parameter | |
3124 * values. | |
3125 */ | |
3126 static Iterable<DataUriParameter> fromMap(Map<String, String> headers) sync* { | |
3127 for (String key in headers.keys) { | |
3128 yield new DataUriParameter(key, headers[key]); | |
3129 } | |
3130 } | |
3131 } | |
OLD | NEW |