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 154 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
165 // segment = *pchar | 165 // segment = *pchar |
166 // segment-nz = 1*pchar | 166 // segment-nz = 1*pchar |
167 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) | 167 // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) |
168 // ; non-zero-length segment without any colon ":" | 168 // ; non-zero-length segment without any colon ":" |
169 // | 169 // |
170 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 170 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
171 // | 171 // |
172 // query = *( pchar / "/" / "?" ) | 172 // query = *( pchar / "/" / "?" ) |
173 // | 173 // |
174 // fragment = *( pchar / "/" / "?" ) | 174 // fragment = *( pchar / "/" / "?" ) |
| 175 bool isRegName(int ch) { |
| 176 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
| 177 } |
175 | 178 |
176 int ipV6Address(int index) { | 179 int ipV6Address(int index) { |
177 // IPv6. Skip to ']'. | 180 // IPv6. Skip to ']'. |
178 int endIndex = uri.indexOf(']', index); | 181 index = uri.indexOf(']', index); |
179 if (endIndex < 0) { | 182 if (index == -1) { |
180 _fail(uri, index - 1, "Unmatched [ in host name"); | 183 throw new FormatException("Bad end of IPv6 host"); |
181 } | 184 } |
182 return endIndex + 1; | 185 return index + 1; |
183 } | 186 } |
184 | 187 |
185 int length = uri.length; | 188 int length = uri.length; |
186 int index = 0; | 189 int index = 0; |
187 | 190 |
188 int schemeEndIndex = 0; | 191 int schemeEndIndex = 0; |
189 | 192 |
190 if (length == 0) { | 193 if (length == 0) { |
191 return new Uri(); | 194 return new Uri(); |
192 } | 195 } |
193 // Whether to allow a colon in the first path segment. | |
194 bool allowColon = false; | |
195 | 196 |
196 if (_isAlphabeticCharacter(uri.codeUnitAt(0))) { | 197 if (uri.codeUnitAt(0) != _SLASH) { |
197 // Can be scheme. | 198 // Can be scheme. |
198 while (index < length) { | 199 while (index < length) { |
199 // Look for ':' to end the scheme. | 200 // Look for ':'. If found, continue from the post of ':'. If not (end |
200 // If found continue from after ':'. | 201 // reached or invalid scheme char found) back up one char, and continue |
201 // If not (end reached or invalid scheme char found) back up one char, | 202 // to path. |
202 // and continue as a path. | |
203 // Note that scheme-chars is contained in path-chars. | 203 // Note that scheme-chars is contained in path-chars. |
204 int codeUnit = uri.codeUnitAt(index++); | 204 int codeUnit = uri.codeUnitAt(index++); |
205 if (!_isSchemeCharacter(codeUnit)) { | 205 if (!_isSchemeCharacter(codeUnit)) { |
206 if (codeUnit == _COLON) { | 206 if (codeUnit == _COLON) { |
207 schemeEndIndex = index; | 207 schemeEndIndex = index; |
208 allowColon = true; // Scheme detected, allow colon in path. | |
209 } else { | 208 } else { |
210 // Back up one char, since we met an invalid scheme char. | 209 // Back up one char, since we met an invalid scheme char. |
211 index--; | 210 index--; |
212 } | 211 } |
213 break; | 212 break; |
214 } | 213 } |
215 } | 214 } |
216 } | 215 } |
217 | 216 |
218 int userInfoEndIndex = -1; | 217 int userInfoEndIndex = -1; |
219 int portIndex = -1; | 218 int portIndex = -1; |
220 int authorityEndIndex = schemeEndIndex; | 219 int authorityEndIndex = schemeEndIndex; |
221 // If we see '//', there must be an authority. | 220 // If we see '//', there must be an authority. |
222 if (authorityEndIndex == index && | 221 if (authorityEndIndex == index && |
223 authorityEndIndex + 1 < length && | 222 authorityEndIndex + 1 < length && |
224 uri.codeUnitAt(authorityEndIndex) == _SLASH && | 223 uri.codeUnitAt(authorityEndIndex) == _SLASH && |
225 uri.codeUnitAt(authorityEndIndex + 1) == _SLASH) { | 224 uri.codeUnitAt(authorityEndIndex + 1) == _SLASH) { |
226 // Skip '//'. | 225 // Skip '//'. |
227 allowColon = true; // First slash seen, allow colon in path. | |
228 authorityEndIndex += 2; | 226 authorityEndIndex += 2; |
229 // It can both be host and userInfo. | 227 // It can both be host and userInfo. |
230 while (authorityEndIndex < length) { | 228 while (authorityEndIndex < length) { |
231 int codeUnit = uri.codeUnitAt(authorityEndIndex++); | 229 int codeUnit = uri.codeUnitAt(authorityEndIndex++); |
232 if (!_isRegNameChar(codeUnit)) { | 230 if (!isRegName(codeUnit)) { |
233 if (codeUnit == _LEFT_BRACKET) { | 231 if (codeUnit == _LEFT_BRACKET) { |
234 authorityEndIndex = ipV6Address(authorityEndIndex); | 232 authorityEndIndex = ipV6Address(authorityEndIndex); |
235 } else if (portIndex == -1 && codeUnit == _COLON) { | 233 } else if (portIndex == -1 && codeUnit == _COLON) { |
236 // First time ':'. | 234 // First time ':'. |
237 portIndex = authorityEndIndex; | 235 portIndex = authorityEndIndex; |
238 } else if (codeUnit == _AT_SIGN || codeUnit == _COLON) { | 236 } else if (codeUnit == _AT_SIGN || codeUnit == _COLON) { |
239 // Second time ':' or first '@'. Must be userInfo. | 237 // Second time ':' or first '@'. Must be userInfo. |
240 if (codeUnit == _AT_SIGN) { | 238 userInfoEndIndex = uri.indexOf('@', authorityEndIndex - 1); |
241 userInfoEndIndex = authorityEndIndex - 1; | 239 // Not found. Must be path then. |
242 } else { | 240 if (userInfoEndIndex == -1) { |
243 userInfoEndIndex = uri.indexOf('@', authorityEndIndex); | 241 authorityEndIndex = index; |
244 // @ Not found after something that can only be userinfo. | 242 break; |
245 if (userInfoEndIndex < 0) { | |
246 _fail(uri, uri.length, "No '@' after userinfo"); | |
247 } | |
248 } | 243 } |
249 portIndex = -1; | 244 portIndex = -1; |
250 authorityEndIndex = userInfoEndIndex + 1; | 245 authorityEndIndex = userInfoEndIndex + 1; |
251 // Now it can only be host:port. | 246 // Now it can only be host:port. |
252 while (authorityEndIndex < length) { | 247 while (authorityEndIndex < length) { |
253 int codeUnit = uri.codeUnitAt(authorityEndIndex++); | 248 int codeUnit = uri.codeUnitAt(authorityEndIndex++); |
254 if (!_isRegNameChar(codeUnit)) { | 249 if (!isRegName(codeUnit)) { |
255 if (codeUnit == _LEFT_BRACKET) { | 250 if (codeUnit == _LEFT_BRACKET) { |
256 authorityEndIndex = ipV6Address(authorityEndIndex); | 251 authorityEndIndex = ipV6Address(authorityEndIndex); |
257 } else if (codeUnit == _COLON) { | 252 } else if (codeUnit == _COLON) { |
258 if (portIndex != -1) { | 253 if (portIndex != -1) { |
259 throw new FormatException("Double port in host"); | 254 throw new FormatException("Double port in host"); |
260 } | 255 } |
261 portIndex = authorityEndIndex; | 256 portIndex = authorityEndIndex; |
262 } else { | 257 } else { |
263 authorityEndIndex--; | 258 authorityEndIndex--; |
264 break; | 259 break; |
265 } | 260 } |
266 } | 261 } |
267 } | 262 } |
268 break; | 263 break; |
269 } else { | 264 } else { |
270 authorityEndIndex--; | 265 authorityEndIndex--; |
271 break; | 266 break; |
272 } | 267 } |
273 } | 268 } |
274 } | 269 } |
275 if (authorityEndIndex < length) { | |
276 // path-abempty - either absolute or empty, so we need a slash if | |
277 // there is a path. | |
278 int codeUnit = uri.codeUnitAt(authorityEndIndex); | |
279 if (codeUnit != _SLASH && | |
280 codeUnit != _QUESTION && | |
281 codeUnit != _NUMBER_SIGN) { | |
282 _fail(uri, authorityEndIndex, "Invalid character in authority"); | |
283 } | |
284 } | |
285 } else { | 270 } else { |
286 authorityEndIndex = schemeEndIndex; | 271 authorityEndIndex = schemeEndIndex; |
287 } | 272 } |
288 | 273 |
289 // At path now. | 274 // At path now. |
290 int pathEndIndex = authorityEndIndex; | 275 int pathEndIndex = authorityEndIndex; |
291 if (!allowColon) { | |
292 while (pathEndIndex < length) { | |
293 int codeUnit = uri.codeUnitAt(pathEndIndex++); | |
294 if (codeUnit == _QUESTION || codeUnit == _NUMBER_SIGN) { | |
295 pathEndIndex--; | |
296 break; | |
297 } | |
298 if (codeUnit == _SLASH) break; | |
299 if (codeUnit == _COLON) { | |
300 _fail(uri, pathEndIndex - 1, "Colon in initial path segment"); | |
301 } | |
302 } | |
303 } | |
304 while (pathEndIndex < length) { | 276 while (pathEndIndex < length) { |
305 int codeUnit = uri.codeUnitAt(pathEndIndex++); | 277 int codeUnit = uri.codeUnitAt(pathEndIndex++); |
306 if (codeUnit == _QUESTION || codeUnit == _NUMBER_SIGN) { | 278 if (codeUnit == _QUESTION || codeUnit == _NUMBER_SIGN) { |
307 pathEndIndex--; | 279 pathEndIndex--; |
308 break; | 280 break; |
309 } | 281 } |
310 } | 282 } |
311 | 283 |
312 // Maybe query. | 284 // Maybe query. |
313 int queryEndIndex = pathEndIndex; | 285 int queryEndIndex = pathEndIndex; |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
361 | 333 |
362 return new Uri(scheme: scheme, | 334 return new Uri(scheme: scheme, |
363 userInfo: userInfo, | 335 userInfo: userInfo, |
364 host: host, | 336 host: host, |
365 port: port, | 337 port: port, |
366 path: path, | 338 path: path, |
367 query: query, | 339 query: query, |
368 fragment: fragment); | 340 fragment: fragment); |
369 } | 341 } |
370 | 342 |
371 // Report a parse failure. | |
372 static void _fail(String uri, int index, String message) { | |
373 // TODO(lrn): Consider adding this to FormatException. | |
374 if (index == uri.length) { | |
375 message += " at end of input."; | |
376 } else { | |
377 message += " at position $index.\n"; | |
378 // Pick a slice of uri containing index and, if | |
379 // necessary, truncate the ends to ensure the entire | |
380 // slice fits on one line. | |
381 int min = 0; | |
382 int max = uri.length; | |
383 String pre = ""; | |
384 String post = ""; | |
385 if (uri.length > 78) { | |
386 min = index - 10; | |
387 if (min < 0) min = 0; | |
388 int max = min + 72; | |
389 if (max > uri.length) { | |
390 max = uri.length; | |
391 min = max - 72; | |
392 } | |
393 if (min != 0) pre = "..."; | |
394 if (max != uri.length) post = "..."; | |
395 } | |
396 // Combine message, slice and a caret pointing to the error index. | |
397 message = "$message$pre${uri.substring(min, max)}$post\n" | |
398 "${' ' * (pre.length + index - min)}^"; | |
399 } | |
400 throw new FormatException(message); | |
401 } | |
402 | |
403 /** | 343 /** |
404 * Creates a new URI from its components. | 344 * Creates a new URI from its components. |
405 * | 345 * |
406 * Each component is set through a named argument. Any number of | 346 * Each component is set through a named argument. Any number of |
407 * components can be provided. The default value for the components | 347 * components can be provided. The default value for the components |
408 * not provided is the empry string, except for [port] which has a | 348 * not provided is the empry string, except for [port] which has a |
409 * default value of 0. The [path] and [query] components can be set | 349 * default value of 0. The [path] and [query] components can be set |
410 * using two different named arguments. | 350 * using two different named arguments. |
411 * | 351 * |
412 * The scheme component is set through [scheme]. The scheme is | 352 * The scheme component is set through [scheme]. The scheme is |
(...skipping 27 matching lines...) Expand all Loading... |
440 * expected to be fully percent-encoded and is used in its literal | 380 * expected to be fully percent-encoded and is used in its literal |
441 * form. When [queryParameters] is used the query is built from the | 381 * form. When [queryParameters] is used the query is built from the |
442 * provided map. Each key and value in the map is percent-encoded | 382 * provided map. Each key and value in the map is percent-encoded |
443 * and joined using equal and ampersand characters. The | 383 * and joined using equal and ampersand characters. The |
444 * percent-encoding of the keys and values encodes all characters | 384 * percent-encoding of the keys and values encodes all characters |
445 * except for the unreserved characters. | 385 * except for the unreserved characters. |
446 * | 386 * |
447 * The fragment component is set through [fragment]. | 387 * The fragment component is set through [fragment]. |
448 */ | 388 */ |
449 Uri({String scheme, | 389 Uri({String scheme, |
450 String userInfo: "", | 390 this.userInfo: "", |
451 String host: "", | 391 String host: "", |
452 port: 0, | 392 port: 0, |
453 String path, | 393 String path, |
454 Iterable<String> pathSegments, | 394 Iterable<String> pathSegments, |
455 String query, | 395 String query, |
456 Map<String, String> queryParameters, | 396 Map<String, String> queryParameters, |
457 fragment: ""}) : | 397 fragment: ""}) : |
458 scheme = _makeScheme(scheme), | 398 scheme = _makeScheme(scheme), |
459 userInfo = _makeUserInfo(userInfo), | |
460 _host = _makeHost(host), | 399 _host = _makeHost(host), |
461 query = _makeQuery(query, queryParameters), | 400 query = _makeQuery(query, queryParameters), |
462 fragment = _makeFragment(fragment) { | 401 fragment = _makeFragment(fragment) { |
463 // Perform scheme specific normalization. | 402 // Perform scheme specific normalization. |
464 if (scheme == "http" && port == 80) { | 403 if (scheme == "http" && port == 80) { |
465 _port = 0; | 404 _port = 0; |
466 } else if (scheme == "https" && port == 443) { | 405 } else if (scheme == "https" && port == 443) { |
467 _port = 0; | 406 _port = 0; |
468 } else { | 407 } else { |
469 _port = port; | 408 _port = port; |
(...skipping 351 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
821 */ | 760 */ |
822 Map<String, String> get queryParameters { | 761 Map<String, String> get queryParameters { |
823 if (_queryParameters == null) { | 762 if (_queryParameters == null) { |
824 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); | 763 _queryParameters = new UnmodifiableMapView(splitQueryString(query)); |
825 } | 764 } |
826 return _queryParameters; | 765 return _queryParameters; |
827 } | 766 } |
828 | 767 |
829 static String _makeHost(String host) { | 768 static String _makeHost(String host) { |
830 if (host == null || host.isEmpty) return host; | 769 if (host == null || host.isEmpty) return host; |
831 // Host is an IPv6 address if it starts with '[' or contains a colon. | |
832 if (host.codeUnitAt(0) == _LEFT_BRACKET) { | 770 if (host.codeUnitAt(0) == _LEFT_BRACKET) { |
833 if (host.codeUnitAt(host.length - 1) != _RIGHT_BRACKET) { | 771 if (host.codeUnitAt(host.length - 1) != _RIGHT_BRACKET) { |
834 throw new FormatException('Missing end `]` to match `[` in host'); | 772 throw new FormatException('Missing end `]` to match `[` in host'); |
835 } | 773 } |
836 parseIPv6Address(host.substring(1, host.length - 1)); | 774 parseIPv6Address(host.substring(1, host.length - 1)); |
837 return host; | 775 return host; |
838 } | 776 } |
839 // TODO(lrn): skip if too short to be a valid IPv6 address. | |
840 for (int i = 0; i < host.length; i++) { | 777 for (int i = 0; i < host.length; i++) { |
841 if (host.codeUnitAt(i) == _COLON) { | 778 if (host.codeUnitAt(i) == _COLON) { |
842 parseIPv6Address(host); | 779 parseIPv6Address(host); |
843 return '[$host]'; | 780 return '[$host]'; |
844 } | 781 } |
845 } | 782 } |
846 return _normalizeRegName(host); | 783 return host; |
847 } | 784 } |
848 | 785 |
849 static bool _isRegNameChar(int char) { | 786 static String _makeScheme(String scheme) { |
850 return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0; | 787 bool isSchemeLowerCharacter(int ch) { |
851 } | 788 return ch < 128 && |
| 789 ((_schemeLowerTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
| 790 } |
852 | 791 |
853 /** | 792 if (scheme == null) return ""; |
854 * Validates and does case- and percent-encoding normalization. | 793 bool allLowercase = true; |
855 * | 794 int length = scheme.length; |
856 * The [host] must be an RFC3986 "reg-name". It is converted | 795 for (int i = 0; i < length; i++) { |
857 * to lower case, and percent escapes are converted to either | 796 int codeUnit = scheme.codeUnitAt(i); |
858 * lower case unreserved characters or upper case escapes. | 797 if (i == 0 && !_isAlphabeticCharacter(codeUnit)) { |
859 */ | 798 // First code unit must be an alphabetic character. |
860 static String _normalizeRegName(String host) { | 799 throw new ArgumentError('Illegal scheme: $scheme'); |
861 StringBuffer buffer; | 800 } |
862 int sectionStart = 0; | 801 if (!isSchemeLowerCharacter(codeUnit)) { |
863 int index = 0; | 802 if (_isSchemeCharacter(codeUnit)) { |
864 // Whether all characters between sectionStart and index are normalized, | 803 allLowercase = false; |
865 bool isNormalized = true; | 804 } else { |
866 | 805 throw new ArgumentError('Illegal scheme: $scheme'); |
867 while (index < host.length) { | |
868 int char = host.codeUnitAt(index); | |
869 if (char == _PERCENT) { | |
870 // The _regNameTable contains "%", so we check that first. | |
871 String replacement = _normalizeEscape(host, index, true); | |
872 if (replacement == null && isNormalized) { | |
873 index += 3; | |
874 continue; | |
875 } | 806 } |
876 if (buffer == null) buffer = new StringBuffer(); | |
877 String slice = host.substring(sectionStart, index); | |
878 if (!isNormalized) slice = slice.toLowerCase(); | |
879 buffer.write(slice); | |
880 if (replacement == null) replacement = host.substring(index, index + 3); | |
881 buffer.write(replacement); | |
882 index += 3; | |
883 sectionStart = index; | |
884 isNormalized = true; | |
885 } else if (_isRegNameChar(char)) { | |
886 if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) { | |
887 // Put initial slice in buffer and continue in non-normalized mode | |
888 if (buffer == null) buffer = new StringBuffer(); | |
889 if (sectionStart < index) { | |
890 buffer.write(host.substring(sectionStart, index)); | |
891 sectionStart = index; | |
892 } | |
893 isNormalized = false; | |
894 } | |
895 index++; | |
896 } else { | |
897 _fail(host, index, "Invalid character"); | |
898 } | 807 } |
899 } | 808 } |
900 if (buffer == null) return host; | |
901 if (sectionStart < host.length) { | |
902 String slice = host.substring(sectionStart); | |
903 if (!isNormalized) slice = slice.toLowerCase(); | |
904 buffer.write(slice); | |
905 } | |
906 return buffer.toString(); | |
907 } | |
908 | 809 |
909 /** | |
910 * Validates scheme characters and does case-normalization. | |
911 * | |
912 * Schemes are converted to lower case. They cannot contain escapes. | |
913 */ | |
914 static String _makeScheme(String scheme) { | |
915 if (scheme == null || scheme.isEmpty) return ""; | |
916 int char = scheme.codeUnitAt(0); | |
917 if (!_isAlphabeticCharacter(char)) { | |
918 _fail(scheme, 0, "Non-alphabetic character starting scheme"); | |
919 } | |
920 bool allLowercase = char > _LOWER_CASE_A; | |
921 for (int i = 0; i < scheme.length; i++) { | |
922 int codeUnit = scheme.codeUnitAt(i); | |
923 if (!_isSchemeCharacter(codeUnit)) { | |
924 _fail(scheme, i, "Illegal scheme character"); | |
925 } | |
926 if (_LOWER_CASE_A <= codeUnit && _LOWER_CASE_Z >= codeUnit) { | |
927 allLowercase = false; | |
928 } | |
929 } | |
930 return allLowercase ? scheme : scheme.toLowerCase(); | 810 return allLowercase ? scheme : scheme.toLowerCase(); |
931 } | 811 } |
932 | 812 |
933 static String _makeUserInfo(String userInfo) { | |
934 if (userInfo == null) return "null"; | |
935 return _normalize(userInfo, _userinfoTable); | |
936 } | |
937 | |
938 static bool _isPathCharacter(int ch) { | |
939 return ch < 128 && ((_pathCharTable[ch >> 4] & (1 << (ch & 0x0f))) != 0) || | |
940 ch == _SLASH; | |
941 } | |
942 | |
943 String _makePath(String path, Iterable<String> pathSegments) { | 813 String _makePath(String path, Iterable<String> pathSegments) { |
944 if (path == null && pathSegments == null) return ""; | 814 if (path == null && pathSegments == null) return ""; |
945 if (path != null && pathSegments != null) { | 815 if (path != null && pathSegments != null) { |
946 throw new ArgumentError('Both path and pathSegments specified'); | 816 throw new ArgumentError('Both path and pathSegments specified'); |
947 } | 817 } |
948 // TODO(lrn): Do path normalization to remove /./ and /../ segments. | |
949 var result; | 818 var result; |
950 if (path != null) { | 819 if (path != null) { |
951 result = _normalize(path, _pathCharOrSlashTable); | 820 result = _normalize(path); |
952 } else { | 821 } else { |
953 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); | 822 result = pathSegments.map((s) => _uriEncode(_pathCharTable, s)).join("/"); |
954 } | 823 } |
955 if ((hasAuthority || (scheme == "file")) && | 824 if ((hasAuthority || (scheme == "file")) && |
956 result.isNotEmpty && !result.startsWith("/")) { | 825 result.isNotEmpty && !result.startsWith("/")) { |
957 return "/$result"; | 826 return "/$result"; |
958 } | 827 } |
959 return result; | 828 return result; |
960 } | 829 } |
961 | 830 |
962 static String _makeQuery(String query, Map<String, String> queryParameters) { | 831 static String _makeQuery(String query, Map<String, String> queryParameters) { |
963 if (query == null && queryParameters == null) return ""; | 832 if (query == null && queryParameters == null) return ""; |
964 if (query != null && queryParameters != null) { | 833 if (query != null && queryParameters != null) { |
965 throw new ArgumentError('Both query and queryParameters specified'); | 834 throw new ArgumentError('Both query and queryParameters specified'); |
966 } | 835 } |
967 if (query != null) return _normalize(query, _queryCharTable); | 836 if (query != null) return _normalize(query); |
968 | 837 |
969 var result = new StringBuffer(); | 838 var result = new StringBuffer(); |
970 var first = true; | 839 var first = true; |
971 queryParameters.forEach((key, value) { | 840 queryParameters.forEach((key, value) { |
972 if (!first) { | 841 if (!first) { |
973 result.write("&"); | 842 result.write("&"); |
974 } | 843 } |
975 first = false; | 844 first = false; |
976 result.write(Uri.encodeQueryComponent(key)); | 845 result.write(Uri.encodeQueryComponent(key)); |
977 if (value != null && !value.isEmpty) { | 846 if (value != null && !value.isEmpty) { |
978 result.write("="); | 847 result.write("="); |
979 result.write(Uri.encodeQueryComponent(value)); | 848 result.write(Uri.encodeQueryComponent(value)); |
980 } | 849 } |
981 }); | 850 }); |
982 return result.toString(); | 851 return result.toString(); |
983 } | 852 } |
984 | 853 |
985 static String _makeFragment(String fragment) { | 854 static String _makeFragment(String fragment) { |
986 if (fragment == null) return ""; | 855 if (fragment == null) return ""; |
987 return _normalize(fragment, _queryCharTable); | 856 return _normalize(fragment); |
988 } | 857 } |
989 | 858 |
990 static bool _isLowerCaseHexDigit(int digit) { | 859 static String _normalize(String component) { |
991 return _LOWER_CASE_A <= digit && digit <= _LOWER_CASE_F; | 860 int index = component.indexOf('%'); |
992 } | 861 if (index < 0) return component; |
993 | 862 |
994 /** Returns whether char is a hex digit. */ | 863 bool isNormalizedHexDigit(int digit) { |
995 static bool _isHexDigit(int char) { | 864 return (_ZERO <= digit && digit <= _NINE) || |
996 if (_NINE >= char) return _ZERO <= char; | 865 (_UPPER_CASE_A <= digit && digit <= _UPPER_CASE_F); |
997 char |= 0x20; | 866 } |
998 return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char; | |
999 } | |
1000 | 867 |
1001 /** Returns value of char as hex digit. */ | 868 bool isLowerCaseHexDigit(int digit) { |
1002 static int _hexValue(int digit) { | 869 return _LOWER_CASE_A <= digit && digit <= _LOWER_CASE_F; |
1003 assert(_isHexDigit(digit)); | 870 } |
1004 if (_NINE >= digit) return digit - _ZERO; | |
1005 return (digit | 0x20) - (_LOWER_CASE_A - 10); | |
1006 } | |
1007 | 871 |
1008 /** | 872 bool isUnreserved(int ch) { |
1009 * Performs RFC 3986 Percent-Encoding Normalization. | 873 return ch < 128 && |
1010 * | 874 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
1011 * Returns a replacement string that should be replace the original escape. | |
1012 * Returns null if no replacement is necessary because the escape is | |
1013 * not for an unreserved character and is already non-lower-case. | |
1014 * | |
1015 * If [lowerCase] is true, a single character returned is always lower case, | |
1016 */ | |
1017 static String _normalizeEscape(String source, int index, bool lowerCase) { | |
1018 assert(source.codeUnitAt(index) == _PERCENT); | |
1019 if (index + 2 >= source.length) { | |
1020 _fail(source, index, "Unterminated percent escape"); | |
1021 } | 875 } |
1022 int firstDigit = source.codeUnitAt(index + 1); | |
1023 int secondDigit = source.codeUnitAt(index + 2); | |
1024 if (!_isHexDigit(firstDigit) || !_isHexDigit(secondDigit)) { | |
1025 _fail(source, index, "Invalid escape"); | |
1026 } | |
1027 int value = _hexValue(firstDigit) * 16 + _hexValue(secondDigit); | |
1028 if (_isUnreservedChar(value)) { | |
1029 if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) { | |
1030 value |= 0x20; | |
1031 } | |
1032 return new String.fromCharCode(value); | |
1033 } | |
1034 if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) { | |
1035 // Either digit is lower case. | |
1036 return source.substring(index, index + 3).toUpperCase(); | |
1037 } | |
1038 return null; | |
1039 } | |
1040 | 876 |
1041 static bool _isUnreservedChar(int ch) { | 877 int normalizeHexDigit(int index) { |
1042 return ch < 127 && | 878 var codeUnit = component.codeUnitAt(index); |
1043 ((_unreservedTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 879 if (isLowerCaseHexDigit(codeUnit)) { |
1044 } | 880 return codeUnit - 0x20; |
1045 | 881 } else if (!isNormalizedHexDigit(codeUnit)) { |
1046 | 882 throw new ArgumentError("Invalid URI component: $component"); |
1047 /** | |
1048 * Runs through component checking that each character is valid and | |
1049 * normalize percent escapes. | |
1050 * | |
1051 * Uses [charTable] to check if a non-`%` character is allowed. | |
1052 * Each `%` character must be followed by two hex digits. | |
1053 * If the hex-digits are lower case letters, they are converted to | |
1054 * upper case. | |
1055 */ | |
1056 static String _normalize(String component, List<int> charTable) { | |
1057 StringBuffer buffer; | |
1058 int sectionStart = 0; | |
1059 int index = 0; | |
1060 // Loop while characters are valid and escapes correct and upper-case. | |
1061 while (index < component.length) { | |
1062 int char = component.codeUnitAt(index); | |
1063 if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) { | |
1064 index++; | |
1065 } else if (char == _PERCENT) { | |
1066 String replacement = _normalizeEscape(component, index, false); | |
1067 if (replacement == null) { | |
1068 // _normalizeEscape returns null if no replacement necessary. | |
1069 index += 3; | |
1070 continue; | |
1071 } else { | |
1072 if (buffer == null) buffer = new StringBuffer(); | |
1073 buffer.write(component.substring(sectionStart, index)); | |
1074 buffer.write(replacement); | |
1075 index += 3; | |
1076 sectionStart = index; | |
1077 } | |
1078 } else { | 883 } else { |
1079 _fail(component, index, "Invalid character"); | 884 return codeUnit; |
1080 } | 885 } |
1081 } | 886 } |
1082 if (buffer == null) return component; | 887 |
1083 if (sectionStart < component.length) { | 888 int decodeHexDigitPair(int index) { |
1084 buffer.write(component.substring(sectionStart)); | 889 int byte = 0; |
| 890 for (int i = 0; i < 2; i++) { |
| 891 var codeUnit = component.codeUnitAt(index + i); |
| 892 if (_ZERO <= codeUnit && codeUnit <= _NINE) { |
| 893 byte = byte * 16 + codeUnit - _ZERO; |
| 894 } else { |
| 895 // Check ranges A-F (0x41-0x46) and a-f (0x61-0x66). |
| 896 codeUnit |= 0x20; |
| 897 if (_LOWER_CASE_A <= codeUnit && |
| 898 codeUnit <= _LOWER_CASE_F) { |
| 899 byte = byte * 16 + codeUnit - _LOWER_CASE_A + 10; |
| 900 } else { |
| 901 throw new ArgumentError( |
| 902 "Invalid percent-encoding in URI component: $component"); |
| 903 } |
| 904 } |
| 905 } |
| 906 return byte; |
1085 } | 907 } |
1086 return buffer.toString(); | 908 |
| 909 // Start building the normalized component string. |
| 910 StringBuffer result; |
| 911 int length = component.length; |
| 912 int prevIndex = 0; |
| 913 |
| 914 // Copy a part of the component string to the result. |
| 915 void fillResult() { |
| 916 if (result == null) { |
| 917 assert(prevIndex == 0); |
| 918 result = new StringBuffer(component.substring(prevIndex, index)); |
| 919 } else { |
| 920 result.write(component.substring(prevIndex, index)); |
| 921 } |
| 922 } |
| 923 |
| 924 while (index < length) { |
| 925 // Normalize percent-encoding to uppercase and don't encode |
| 926 // unreserved characters. |
| 927 assert(component.codeUnitAt(index) == _PERCENT); |
| 928 if (length < index + 2) { |
| 929 throw new ArgumentError( |
| 930 "Invalid percent-encoding in URI component: $component"); |
| 931 } |
| 932 |
| 933 var codeUnit1 = component.codeUnitAt(index + 1); |
| 934 var codeUnit2 = component.codeUnitAt(index + 2); |
| 935 var decodedCodeUnit = decodeHexDigitPair(index + 1); |
| 936 if (isNormalizedHexDigit(codeUnit1) && |
| 937 isNormalizedHexDigit(codeUnit2) && |
| 938 !isUnreserved(decodedCodeUnit)) { |
| 939 index += 3; |
| 940 } else { |
| 941 fillResult(); |
| 942 if (isUnreserved(decodedCodeUnit)) { |
| 943 result.writeCharCode(decodedCodeUnit); |
| 944 } else { |
| 945 result.write("%"); |
| 946 result.writeCharCode(normalizeHexDigit(index + 1)); |
| 947 result.writeCharCode(normalizeHexDigit(index + 2)); |
| 948 } |
| 949 index += 3; |
| 950 prevIndex = index; |
| 951 } |
| 952 int next = component.indexOf('%', index); |
| 953 if (next >= index) { |
| 954 index = next; |
| 955 } else { |
| 956 index = length; |
| 957 } |
| 958 } |
| 959 if (result == null) return component; |
| 960 |
| 961 if (result != null && prevIndex != index) fillResult(); |
| 962 assert(index == length); |
| 963 |
| 964 return result.toString(); |
1087 } | 965 } |
1088 | 966 |
1089 static bool _isSchemeCharacter(int ch) { | 967 static bool _isSchemeCharacter(int ch) { |
1090 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); | 968 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); |
1091 } | 969 } |
1092 | 970 |
| 971 |
1093 /** | 972 /** |
1094 * Returns whether the URI is absolute. | 973 * Returns whether the URI is absolute. |
1095 */ | 974 */ |
1096 bool get isAbsolute => scheme != "" && fragment == ""; | 975 bool get isAbsolute => scheme != "" && fragment == ""; |
1097 | 976 |
1098 String _merge(String base, String reference) { | 977 String _merge(String base, String reference) { |
1099 if (base == "") return "/$reference"; | 978 if (base == "") return "/$reference"; |
1100 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference"; | 979 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference"; |
1101 } | 980 } |
1102 | 981 |
(...skipping 606 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1709 | 1588 |
1710 /** | 1589 /** |
1711 * This is the internal implementation of JavaScript's encodeURI function. | 1590 * This is the internal implementation of JavaScript's encodeURI function. |
1712 * It encodes all characters in the string [text] except for those | 1591 * It encodes all characters in the string [text] except for those |
1713 * that appear in [canonicalTable], and returns the escaped string. | 1592 * that appear in [canonicalTable], and returns the escaped string. |
1714 */ | 1593 */ |
1715 static String _uriEncode(List<int> canonicalTable, | 1594 static String _uriEncode(List<int> canonicalTable, |
1716 String text, | 1595 String text, |
1717 {Encoding encoding: UTF8, | 1596 {Encoding encoding: UTF8, |
1718 bool spaceToPlus: false}) { | 1597 bool spaceToPlus: false}) { |
1719 void byteToHex(byte, buffer) { | 1598 byteToHex(byte, buffer) { |
1720 const String hex = '0123456789ABCDEF'; | 1599 const String hex = '0123456789ABCDEF'; |
1721 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); | 1600 buffer.writeCharCode(hex.codeUnitAt(byte >> 4)); |
1722 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); | 1601 buffer.writeCharCode(hex.codeUnitAt(byte & 0x0f)); |
1723 } | 1602 } |
1724 | 1603 |
1725 // Encode the string into bytes then generate an ASCII only string | 1604 // Encode the string into bytes then generate an ASCII only string |
1726 // by percent encoding selected bytes. | 1605 // by percent encoding selected bytes. |
1727 StringBuffer result = new StringBuffer(); | 1606 StringBuffer result = new StringBuffer(); |
1728 var bytes = encoding.encode(text); | 1607 var bytes = encoding.encode(text); |
1729 for (int i = 0; i < bytes.length; i++) { | 1608 for (int i = 0; i < bytes.length; i++) { |
1730 int byte = bytes[i]; | 1609 int byte = bytes[i]; |
1731 if (byte < 128) { | 1610 if (byte < 128 && |
1732 if ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0) { | 1611 ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
1733 result.writeCharCode(byte); | 1612 result.writeCharCode(byte); |
1734 continue; | 1613 } else if (spaceToPlus && byte == _SPACE) { |
1735 } | 1614 result.writeCharCode(_PLUS); |
1736 if (spaceToPlus && byte == _SPACE) { | 1615 } else { |
1737 result.writeCharCode(_PLUS); | 1616 result.writeCharCode(_PERCENT); |
1738 continue; | 1617 byteToHex(byte, result); |
1739 } | |
1740 } | 1618 } |
1741 result.writeCharCode(_PERCENT); | |
1742 byteToHex(byte, result); | |
1743 } | 1619 } |
1744 return result.toString(); | 1620 return result.toString(); |
1745 } | 1621 } |
1746 | 1622 |
1747 /** | 1623 /** |
1748 * Convert a byte (2 character hex sequence) in string [s] starting | 1624 * Convert a byte (2 character hex sequence) in string [s] starting |
1749 * at position [pos] to its ordinal value | 1625 * at position [pos] to its ordinal value |
1750 */ | 1626 */ |
1751 static int _hexCharPairToByte(String s, int pos) { | 1627 static int _hexCharPairToByte(String s, int pos) { |
1752 int byte = 0; | 1628 int byte = 0; |
(...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1958 0x2bff, // 0x30 - 0x3f 1111111111010100 | 1834 0x2bff, // 0x30 - 0x3f 1111111111010100 |
1959 // ABCDEFGHIJKLMNO | 1835 // ABCDEFGHIJKLMNO |
1960 0xfffe, // 0x40 - 0x4f 0111111111111111 | 1836 0xfffe, // 0x40 - 0x4f 0111111111111111 |
1961 // PQRSTUVWXYZ _ | 1837 // PQRSTUVWXYZ _ |
1962 0x87ff, // 0x50 - 0x5f 1111111111100001 | 1838 0x87ff, // 0x50 - 0x5f 1111111111100001 |
1963 // abcdefghijklmno | 1839 // abcdefghijklmno |
1964 0xfffe, // 0x60 - 0x6f 0111111111111111 | 1840 0xfffe, // 0x60 - 0x6f 0111111111111111 |
1965 // pqrstuvwxyz ~ | 1841 // pqrstuvwxyz ~ |
1966 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 1842 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
1967 | 1843 |
1968 // Characters allowed in the userinfo as of RFC 3986. | |
1969 // RFC 3986 Apendix A | |
1970 // userinfo = *( unreserved / pct-encoded / sub-delims / ':') | |
1971 static const _userinfoTable = const [ | |
1972 // LSB MSB | |
1973 // | | | |
1974 0x0000, // 0x00 - 0x0f 0000000000000000 | |
1975 0x0000, // 0x10 - 0x1f 0000000000000000 | |
1976 // ! $ &'()*+,-. | |
1977 0x7fd2, // 0x20 - 0x2f 0100101111111110 | |
1978 // 0123456789:; = | |
1979 0x2fff, // 0x30 - 0x3f 1111111111110100 | |
1980 // ABCDEFGHIJKLMNO | |
1981 0xfffe, // 0x40 - 0x4f 0111111111111111 | |
1982 // PQRSTUVWXYZ _ | |
1983 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
1984 // abcdefghijklmno | |
1985 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
1986 // pqrstuvwxyz ~ | |
1987 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
1988 | |
1989 // Characters allowed in the path as of RFC 3986. | 1844 // Characters allowed in the path as of RFC 3986. |
1990 // RFC 3986 section 3.3. | 1845 // RFC 3986 section 3.3. |
1991 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | 1846 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" |
1992 static const _pathCharTable = const [ | 1847 static const _pathCharTable = const [ |
1993 // LSB MSB | 1848 // LSB MSB |
1994 // | | | 1849 // | | |
1995 0x0000, // 0x00 - 0x0f 0000000000000000 | 1850 0x0000, // 0x00 - 0x0f 0000000000000000 |
1996 0x0000, // 0x10 - 0x1f 0000000000000000 | 1851 0x0000, // 0x10 - 0x1f 0000000000000000 |
1997 // ! $ &'()*+,-. | 1852 // ! $ &'()*+,-. |
1998 0x7fd2, // 0x20 - 0x2f 0100101111111110 | 1853 0x7fd2, // 0x20 - 0x2f 0100101111111110 |
1999 // 0123456789:; = | 1854 // 0123456789:; = |
2000 0x2fff, // 0x30 - 0x3f 1111111111110100 | 1855 0x2fff, // 0x30 - 0x3f 1111111111110100 |
2001 // @ABCDEFGHIJKLMNO | 1856 // @ABCDEFGHIJKLMNO |
2002 0xffff, // 0x40 - 0x4f 1111111111111111 | 1857 0xffff, // 0x40 - 0x4f 1111111111111111 |
2003 // PQRSTUVWXYZ _ | 1858 // PQRSTUVWXYZ _ |
2004 0x87ff, // 0x50 - 0x5f 1111111111100001 | 1859 0x87ff, // 0x50 - 0x5f 1111111111100001 |
2005 // abcdefghijklmno | 1860 // abcdefghijklmno |
2006 0xfffe, // 0x60 - 0x6f 0111111111111111 | 1861 0xfffe, // 0x60 - 0x6f 0111111111111111 |
2007 // pqrstuvwxyz ~ | 1862 // pqrstuvwxyz ~ |
2008 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 1863 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
2009 | 1864 |
2010 // Characters allowed in the path as of RFC 3986. | |
2011 // RFC 3986 section 3.3 *and* slash. | |
2012 static const _pathCharOrSlashTable = const [ | |
2013 // LSB MSB | |
2014 // | | | |
2015 0x0000, // 0x00 - 0x0f 0000000000000000 | |
2016 0x0000, // 0x10 - 0x1f 0000000000000000 | |
2017 // ! $ &'()*+,-./ | |
2018 0xffd2, // 0x20 - 0x2f 0100101111111111 | |
2019 // 0123456789:; = | |
2020 0x2fff, // 0x30 - 0x3f 1111111111110100 | |
2021 // @ABCDEFGHIJKLMNO | |
2022 0xffff, // 0x40 - 0x4f 1111111111111111 | |
2023 // PQRSTUVWXYZ _ | |
2024 0x87ff, // 0x50 - 0x5f 1111111111100001 | |
2025 // abcdefghijklmno | |
2026 0xfffe, // 0x60 - 0x6f 0111111111111111 | |
2027 // pqrstuvwxyz ~ | |
2028 0x47ff]; // 0x70 - 0x7f 1111111111100010 | |
2029 | |
2030 // Characters allowed in the query as of RFC 3986. | 1865 // Characters allowed in the query as of RFC 3986. |
2031 // RFC 3986 section 3.4. | 1866 // RFC 3986 section 3.4. |
2032 // query = *( pchar / "/" / "?" ) | 1867 // query = *( pchar / "/" / "?" ) |
2033 static const _queryCharTable = const [ | 1868 static const _queryCharTable = const [ |
2034 // LSB MSB | 1869 // LSB MSB |
2035 // | | | 1870 // | | |
2036 0x0000, // 0x00 - 0x0f 0000000000000000 | 1871 0x0000, // 0x00 - 0x0f 0000000000000000 |
2037 0x0000, // 0x10 - 0x1f 0000000000000000 | 1872 0x0000, // 0x10 - 0x1f 0000000000000000 |
2038 // ! $ &'()*+,-./ | 1873 // ! $ &'()*+,-./ |
2039 0xffd2, // 0x20 - 0x2f 0100101111111111 | 1874 0xffd2, // 0x20 - 0x2f 0100101111111111 |
2040 // 0123456789:; = ? | 1875 // 0123456789:; = ? |
2041 0xafff, // 0x30 - 0x3f 1111111111110101 | 1876 0xafff, // 0x30 - 0x3f 1111111111110101 |
2042 // @ABCDEFGHIJKLMNO | 1877 // @ABCDEFGHIJKLMNO |
2043 0xffff, // 0x40 - 0x4f 1111111111111111 | 1878 0xffff, // 0x40 - 0x4f 1111111111111111 |
2044 // PQRSTUVWXYZ _ | 1879 // PQRSTUVWXYZ _ |
2045 0x87ff, // 0x50 - 0x5f 1111111111100001 | 1880 0x87ff, // 0x50 - 0x5f 1111111111100001 |
2046 // abcdefghijklmno | 1881 // abcdefghijklmno |
2047 0xfffe, // 0x60 - 0x6f 0111111111111111 | 1882 0xfffe, // 0x60 - 0x6f 0111111111111111 |
2048 // pqrstuvwxyz ~ | 1883 // pqrstuvwxyz ~ |
2049 0x47ff]; // 0x70 - 0x7f 1111111111100010 | 1884 0x47ff]; // 0x70 - 0x7f 1111111111100010 |
2050 } | 1885 } |
OLD | NEW |