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

Side by Side Diff: sdk/lib/core/uri.dart

Issue 321543003: New, more validating, parser for URI. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Update and add tests. Created 6 years, 6 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 | Annotate | Revision Log
OLDNEW
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 100 matching lines...) Expand 10 before | Expand all | Expand 10 after
111 List<String> _pathSegments; 111 List<String> _pathSegments;
112 112
113 /** 113 /**
114 * Cache the computed return value of [queryParameters]. 114 * Cache the computed return value of [queryParameters].
115 */ 115 */
116 Map<String, String> _queryParameters; 116 Map<String, String> _queryParameters;
117 117
118 /** 118 /**
119 * Creates a new URI object by parsing a URI string. 119 * Creates a new URI object by parsing a URI string.
120 */ 120 */
121 static Uri parse(String uri) { 121 static Uri parse(String uri) {
kevmoo 2014/06/10 21:29:08 Need to update doc comment that this method will n
Lasse Reichstein Nielsen 2014/06/12 08:07:31 It threw before as well, now it just detects more
122 // This parsing will not validate percent-encoding, IPv6, etc. When done 122 // This parsing will not validate percent-encoding, IPv6, etc. When done
123 // it will call `new Uri(...)` which will perform these validations. 123 // it will call `new Uri(...)` which will perform these validations.
124 // This is purely splitting up the URI string into components. 124 // This is purely splitting up the URI string into components.
125 // 125 //
126 // Important parts of the RFC 3986 used here: 126 // Important parts of the RFC 3986 used here:
127 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 127 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
128 // 128 //
129 // hier-part = "//" authority path-abempty 129 // hier-part = "//" authority path-abempty
130 // / path-absolute 130 // / path-absolute
131 // / path-rootless 131 // / path-rootless
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
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) { 175 if (uri.isEmpty) {
176 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); 176 return new Uri();
177 } 177 }
178 178 String scheme;
179 int ipV6Address(int index) { 179 String userInfo = "";
180 // IPv6. Skip to ']'. 180 String host = "";
181 index = uri.indexOf(']', index); 181 int port = 0;
182 if (index == -1) { 182 String path = "";
183 throw new FormatException("Bad end of IPv6 host"); 183 String query;
184 } 184 String fragment;
185 return index + 1; 185 bool allowColonInPath = false;
186 } 186
187 187 /// Index after current char.
Søren Gjesse 2014/06/11 08:33:15 Why dart-doc comments here?
Lasse Reichstein Nielsen 2014/06/12 08:07:31 Because I assume the editor will display that comm
188 int length = uri.length;
189 int index = 0; 188 int index = 0;
190 189 /// Current char.
191 int schemeEndIndex = 0; 190 int current = _EOI;
192 191 /// Start index of current section being parsed.
193 if (length == 0) { 192 int start = 0;
Anders Johnsen 2014/06/11 10:00:03 Start is a bit ambiguous. currentSectionStart ?
Lasse Reichstein Nielsen 2014/06/12 08:07:31 .... loooooong name. But ok.
194 return new Uri(); 193 /// The position after the current character. If parsing a percent
195 } 194 /// escape, this is index + 3, otherwise index + 1.
196 195 int nextIndex = 0;
197 if (uri.codeUnitAt(0) != _SLASH) { 196
198 // Can be scheme. 197 void advance() {
199 while (index < length) { 198 index = nextIndex;
200 // Look for ':'. If found, continue from the post of ':'. If not (end 199 if (index < uri.length) {
201 // reached or invalid scheme char found) back up one char, and continue 200 current = uri.codeUnitAt(index);
202 // to path. 201 if (_MAX_VALID_CHAR >= current) {
203 // Note that scheme-chars is contained in path-chars. 202 if (_PERCENT != current) {
204 int codeUnit = uri.codeUnitAt(index++); 203 nextIndex = nextIndex + 1;
205 if (!_isSchemeCharacter(codeUnit)) { 204 return;
206 if (codeUnit == _COLON) { 205 }
207 schemeEndIndex = index; 206 if (index + 2 >= uri.length ||
208 } else { 207 !_isHexDigit(uri.codeUnitAt(index + 1)) ||
209 // Back up one char, since we met an invalid scheme char. 208 !_isHexDigit(uri.codeUnitAt(index + 2))) {
210 index--; 209 _fail(uri, index, "Incomplete percent escape");
211 } 210 }
212 break; 211 // Valid escape, returns _PERCENT as current.
213 } 212 nextIndex = nextIndex + 3;
214 } 213 return;
215 } 214 }
216 215 _fail(uri, index, "Unexpected character");
217 int userInfoEndIndex = -1; 216 }
218 int portIndex = -1; 217 current = _EOI;
219 int authorityEndIndex = schemeEndIndex; 218 }
220 // If we see '//', there must be an authority. 219
221 if (authorityEndIndex == index && 220 // Parse authority.
222 authorityEndIndex + 1 < length && 221 void parseAuth() {
223 uri.codeUnitAt(authorityEndIndex) == _SLASH && 222 start = index;
224 uri.codeUnitAt(authorityEndIndex + 1) == _SLASH) { 223 void parseIpV6() {
225 // Skip '//'. 224 assert(current == _LEFT_BRACKET);
226 authorityEndIndex += 2; 225 assert(start == index);
227 // It can both be host and userInfo. 226 for (int i = index + 1; i < uri.length; i++) {
228 while (authorityEndIndex < length) { 227 if (uri.codeUnitAt(i) == _RIGHT_BRACKET) {
229 int codeUnit = uri.codeUnitAt(authorityEndIndex++); 228 nextIndex = i + 1;
230 if (!isRegName(codeUnit)) { 229 advance();
231 if (codeUnit == _LEFT_BRACKET) { 230 host = uri.substring(start, index);
Søren Gjesse 2014/06/11 08:33:15 Are we doing proper IPv6 validation later, or is t
Lasse Reichstein Nielsen 2014/06/12 08:07:31 I believe we are doing it later. I don't know if
232 authorityEndIndex = ipV6Address(authorityEndIndex); 231 return;
233 } else if (portIndex == -1 && codeUnit == _COLON) { 232 }
234 // First time ':'. 233 }
235 portIndex = authorityEndIndex; 234 _fail(uri, start, "Unmatched [ in host name");
kevmoo 2014/06/10 21:29:08 quote '['
236 } else if (codeUnit == _AT_SIGN || codeUnit == _COLON) { 235 }
237 // Second time ':' or first '@'. Must be userInfo. 236
238 userInfoEndIndex = uri.indexOf('@', authorityEndIndex - 1); 237 void parseHost() {
239 // Not found. Must be path then. 238 assert(start == index);
240 if (userInfoEndIndex == -1) { 239 if (current == _LEFT_BRACKET) {
241 authorityEndIndex = index; 240 parseIpV6();
242 break; 241 return;
242 }
243 while (_isRegNameChar(current)) {
244 advance();
245 }
246 host = uri.substring(start, index);
247 }
248
249 int parsePort() {
250 assert(_isDigit(current));
251 int portVal = current - _ZERO;
252 advance();
253 while (_isDigit(current)) {
254 portVal = portVal * 10 + (current - _ZERO);
Søren Gjesse 2014/06/11 08:33:15 Any limitations on the port value in the spec?
255 advance();
256 }
257 return portVal;
258 }
259
260 maybeUserInfo: {
261 // Break this when we know user-info is done or not there.
262 // user-info or reg-name
263 if (current == _LEFT_BRACKET) break maybeUserInfo;
264 while (_isRegNameChar(current)) {
265 advance();
266 }
267 if (current == _SLASH) {
268 host = uri.substring(start, index);
269 return;
270 }
271 if (current == _AT_SIGN) {
272 userInfo = uri.substring(start, index);
273 advance();
274 start = index;
275 break maybeUserInfo;
276 }
277 if (current == _COLON) {
278 // First colon seen after what might be a host name.
279 // Can be either part of user-info or preceeding a port.
280 int hostEnd = index;
281 advance();
282 // user-info or port.
283 if (_isDigit(current)) {
284 int portVal = parsePort();
285 if (current == _SLASH || current == _EOI) {
286 host = uri.substring(start, hostEnd);
287 port = portVal;
288 return;
243 } 289 }
244 portIndex = -1; 290 }
245 authorityEndIndex = userInfoEndIndex + 1; 291 }
246 // Now it can only be host:port. 292 if (current == _EOI) {
247 while (authorityEndIndex < length) { 293 host = uri.substring(start, index);
248 int codeUnit = uri.codeUnitAt(authorityEndIndex++); 294 return;
249 if (!isRegName(codeUnit)) { 295 }
250 if (codeUnit == _LEFT_BRACKET) { 296 // A non-port character seen after a colon.
251 authorityEndIndex = ipV6Address(authorityEndIndex); 297 // This must be user-info.
252 } else if (codeUnit == _COLON) { 298 while (_isUserInfoChar(current)) {
253 if (portIndex != -1) { 299 advance();
254 throw new FormatException("Double port in host"); 300 }
255 } 301 if (current != _AT_SIGN) {
256 portIndex = authorityEndIndex; 302 _fail(uri, index, "Expected @");
kevmoo 2014/06/10 21:29:08 quote '@'
257 } else { 303 }
258 authorityEndIndex--; 304 userInfo = uri.substring(start, index);
259 break; 305 advance();
260 } 306 start = index;
261 } 307 } // end maybeUserInfo
262 } 308 parseHost();
263 break; 309 if (current == _COLON) {
264 } else { 310 advance();
265 authorityEndIndex--; 311 if (!_isDigit(current)) {
266 break; 312 _fail(uri, index, "Expected port number");
267 } 313 }
268 } 314 port = parsePort();
269 } 315 }
270 } else { 316 } // end parseAuth().
271 authorityEndIndex = schemeEndIndex; 317
272 } 318 // Start parsing.
273 319 to_path: { // Break this block to go to parsing the path.
274 // At path now. 320 advance();
275 int pathEndIndex = authorityEndIndex; 321 if (_isAlpha(current)) {
Anders Johnsen 2014/06/11 10:00:03 Do we need 'scheme != null' to prevent double-pars
Lasse Reichstein Nielsen 2014/06/12 08:07:31 No, there is no parsing before this point. The cod
276 while (pathEndIndex < length) { 322 // May be scheme or path.
277 int codeUnit = uri.codeUnitAt(pathEndIndex++); 323 do {
278 if (codeUnit == _QUESTION || codeUnit == _NUMBER_SIGN) { 324 advance();
279 pathEndIndex--; 325 } while (_isSchemeCharacter(current));
280 break; 326 if (current != _COLON) {
281 } 327 break to_path;
282 } 328 }
283 329 allowColonInPath = true;
284 // Maybe query. 330 scheme = uri.substring(0, index);
285 int queryEndIndex = pathEndIndex; 331 advance();
286 if (queryEndIndex < length && uri.codeUnitAt(queryEndIndex) == _QUESTION) { 332 start = index;
287 while (queryEndIndex < length) { 333 }
288 int codeUnit = uri.codeUnitAt(queryEndIndex++); 334 // Path or authority.
289 if (codeUnit == _NUMBER_SIGN) { 335 if (current == _SLASH) {
290 queryEndIndex--; 336 allowColonInPath = true;
291 break; 337 advance();
292 } 338 if (current == _SLASH) {
293 } 339 advance();
294 } 340 parseAuth();
295 341 if (current == _EOI) {
296 var scheme = null; 342 start = index;
297 if (schemeEndIndex > 0) { 343 break to_path;
298 scheme = uri.substring(0, schemeEndIndex - 1); 344 }
299 } 345 if (current != _SLASH) {
300 346 _fail(uri, index, "Expected /");
kevmoo 2014/06/10 21:29:08 quote '/'
301 var host = ""; 347 }
302 var userInfo = ""; 348 start = index;
303 var port = 0; 349 advance();
304 if (schemeEndIndex != authorityEndIndex) { 350 }
305 int startIndex = schemeEndIndex + 2; 351 }
306 if (userInfoEndIndex > 0) { 352 } // end to_path.
307 userInfo = uri.substring(startIndex, userInfoEndIndex); 353 if (!allowColonInPath) {
308 startIndex = userInfoEndIndex + 1; 354 while (_isPathChar(current) || current == _PERCENT) {
Anders Johnsen 2014/06/11 10:00:03 Will _isPathChar return true for '/' ?
Lasse Reichstein Nielsen 2014/06/12 08:07:31 No.
309 } 355 if (current == _COLON) {
310 if (portIndex > 0) { 356 _fail(uri, index, "Colon in path before first '/'");
311 var portStr = uri.substring(portIndex, authorityEndIndex); 357 }
312 try { 358 advance();
313 port = int.parse(portStr); 359 }
314 } catch (_) { 360 }
315 throw new FormatException("Invalid port: '$portStr'"); 361 // Start or continue path. May be empty.
316 } 362 while (_isPathChar(current) || current == _SLASH || current == _PERCENT) {
317 host = uri.substring(startIndex, portIndex - 1); 363 advance();
318 } else { 364 }
319 host = uri.substring(startIndex, authorityEndIndex); 365 path = uri.substring(start, index);
320 } 366
321 } 367 if (current == _QUESTION) {
322 368 start = nextIndex;
323 var path = uri.substring(authorityEndIndex, pathEndIndex); 369 do {
324 var query = ""; 370 advance();
325 if (pathEndIndex < queryEndIndex) { 371 } while (_isQueryChar(current) || current == _PERCENT);
326 query = uri.substring(pathEndIndex + 1, queryEndIndex); 372 query = uri.substring(start, index);
327 } 373 }
328 var fragment = ""; 374
329 // If queryEndIndex is not at end (length), there is a fragment. 375 if (current == _NUMBER_SIGN) {
330 if (queryEndIndex < length) { 376 // Fragment can contain same characters as query.
Søren Gjesse 2014/06/11 08:33:15 No number sign in fragment?
Lasse Reichstein Nielsen 2014/06/12 08:07:32 No. At most one # in any URL.
331 fragment = uri.substring(queryEndIndex + 1, length); 377 start = nextIndex;
378 do {
379 advance();
380 } while (_isQueryChar(current) || current == _PERCENT);
381 fragment = uri.substring(start, index);
382 }
383
384 if (current != _EOI) {
385 _fail(uri, index, "Unexpected character");
332 } 386 }
333 387
334 return new Uri(scheme: scheme, 388 return new Uri(scheme: scheme,
Anders Johnsen 2014/06/11 10:00:03 This constructor does some extra validation. Do we
Lasse Reichstein Nielsen 2014/06/12 08:07:31 As pointed out elsewhere, the validation needs to
335 userInfo: userInfo, 389 userInfo: userInfo,
390 port: port,
kevmoo 2014/06/10 21:29:08 nit: might as well keep these params in the origin
Lasse Reichstein Nielsen 2014/06/12 08:07:31 ACK.
336 host: host, 391 host: host,
337 port: port,
338 path: path, 392 path: path,
339 query: query, 393 query: query,
340 fragment: fragment); 394 fragment: fragment);
341 } 395 }
342 396
397 // Report a parse failure.
398 static void _fail(uri, index, message) {
kevmoo 2014/06/10 21:29:08 Add types to parameters
399 if (index == uri.length) {
400 message += ": Unexpected end of input.";
kevmoo 2014/06/10 21:29:08 Could this be changed so each message reads like a
401 } else {
402 message += ": Unexpected character at position $index.\n";
kevmoo 2014/06/10 21:29:08 ditto for reading like a sentence from above.
403 int min = 0;
404 int max = uri.length;
405 String pre = "";
406 String post = "";
kevmoo 2014/06/10 21:29:08 Code comments for what you're doing here? Is this
Lasse Reichstein Nielsen 2014/06/12 08:07:31 Yes, at most 78 characters of the source will be d
407 if (uri.length > 78) {
408 min = index - 10;
409 if (min < 0) min = 0;
410 int max = min + 72;
411 if (max > uri.length) {
412 max = uri.length;
413 min = max - 72;
414 }
415 if (min != 0) pre = "...";
416 if (max != uri.length) post = "...";
417 }
418 message = "$message$pre${uri.substring(min, max)}$post\n"
419 "${' ' * (pre.length + index - min)}^";
420 }
421 throw new FormatException(message);
422 }
423
424
343 /** 425 /**
344 * Creates a new URI from its components. 426 * Creates a new URI from its components.
345 * 427 *
346 * Each component is set through a named argument. Any number of 428 * Each component is set through a named argument. Any number of
347 * components can be provided. The default value for the components 429 * components can be provided. The default value for the components
348 * not provided is the empry string, except for [port] which has a 430 * not provided is the empry string, except for [port] which has a
349 * default value of 0. The [path] and [query] components can be set 431 * default value of 0. The [path] and [query] components can be set
350 * using two different named arguments. 432 * using two different named arguments.
351 * 433 *
352 * The scheme component is set through [scheme]. The scheme is 434 * The scheme component is set through [scheme]. The scheme is
(...skipping 574 matching lines...) Expand 10 before | Expand all | Expand 10 after
927 if (result != null && prevIndex != index) fillResult(); 1009 if (result != null && prevIndex != index) fillResult();
928 assert(index == length); 1010 assert(index == length);
929 1011
930 return result.toString(); 1012 return result.toString();
931 } 1013 }
932 1014
933 static bool _isSchemeCharacter(int ch) { 1015 static bool _isSchemeCharacter(int ch) {
934 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0); 1016 return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
935 } 1017 }
936 1018
1019 static bool _isPathChar(int ch) {
1020 return ch < 128 && ((_pathCharTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
1021 }
1022
1023 static bool _isRegNameChar(int ch) {
1024 return ch < 128 && ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
1025 }
1026
1027 static bool _isUserInfoChar(int ch) {
1028 return ch < 128 && (
1029 ((_regNameTable[ch >> 4] & (1 << (ch & 0x0f))) != 0) || ch == _COLON);
1030 }
1031
1032 static bool _isQueryChar(int ch) {
1033 return ch < 128 && ((_queryCharTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
1034 }
937 1035
938 /** 1036 /**
939 * Returns whether the URI is absolute. 1037 * Returns whether the URI is absolute.
940 */ 1038 */
941 bool get isAbsolute => scheme != "" && fragment == ""; 1039 bool get isAbsolute => scheme != "" && fragment == "";
942 1040
943 String _merge(String base, String reference) { 1041 String _merge(String base, String reference) {
944 if (base == "") return "/$reference"; 1042 if (base == "") return "/$reference";
945 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference"; 1043 return "${base.substring(0, base.lastIndexOf("/") + 1)}$reference";
946 } 1044 }
(...skipping 597 matching lines...) Expand 10 before | Expand all | Expand 10 after
1544 static const int _UPPER_CASE_A = 0x41; 1642 static const int _UPPER_CASE_A = 0x41;
1545 static const int _UPPER_CASE_F = 0x46; 1643 static const int _UPPER_CASE_F = 0x46;
1546 static const int _UPPER_CASE_Z = 0x5A; 1644 static const int _UPPER_CASE_Z = 0x5A;
1547 static const int _LEFT_BRACKET = 0x5B; 1645 static const int _LEFT_BRACKET = 0x5B;
1548 static const int _BACKSLASH = 0x5C; 1646 static const int _BACKSLASH = 0x5C;
1549 static const int _RIGHT_BRACKET = 0x5D; 1647 static const int _RIGHT_BRACKET = 0x5D;
1550 static const int _LOWER_CASE_A = 0x61; 1648 static const int _LOWER_CASE_A = 0x61;
1551 static const int _LOWER_CASE_F = 0x66; 1649 static const int _LOWER_CASE_F = 0x66;
1552 static const int _LOWER_CASE_Z = 0x7A; 1650 static const int _LOWER_CASE_Z = 0x7A;
1553 static const int _BAR = 0x7C; 1651 static const int _BAR = 0x7C;
1652 static const int _MAX_VALID_CHAR = 0x7e;
1653 static const int _EOI = 0x10000; // Not a code unit.
Anders Johnsen 2014/06/11 10:00:03 -1?
Lasse Reichstein Nielsen 2014/06/12 08:07:31 Breaks the _isPathChar function (or requires an ex
1654
1655 static bool _isAlpha(int char) {
1656 char |= 0x20;
1657 return _LOWER_CASE_A <= char && _LOWER_CASE_Z >= char;
1658 // TODO: Test:
1659 // return ((char - _a) & 0xffff) <= (_z - _a);
1660 }
1661
1662 static bool _isDigit(int char) {
1663 return _NINE >= char && _ZERO <= char;
1664 }
1665
1666 static bool _isHexDigit(int char) {
1667 if (_NINE >= char) return _ZERO <= char;
1668 char |= 0x20;
1669 return _LOWER_CASE_A <= char && _LOWER_CASE_F >= char;
1670 }
1554 1671
1555 /** 1672 /**
1556 * This is the internal implementation of JavaScript's encodeURI function. 1673 * This is the internal implementation of JavaScript's encodeURI function.
1557 * It encodes all characters in the string [text] except for those 1674 * It encodes all characters in the string [text] except for those
1558 * that appear in [canonicalTable], and returns the escaped string. 1675 * that appear in [canonicalTable], and returns the escaped string.
1559 */ 1676 */
1560 static String _uriEncode(List<int> canonicalTable, 1677 static String _uriEncode(List<int> canonicalTable,
1561 String text, 1678 String text,
1562 {Encoding encoding: UTF8, 1679 {Encoding encoding: UTF8,
1563 bool spaceToPlus: false}) { 1680 bool spaceToPlus: false}) {
(...skipping 278 matching lines...) Expand 10 before | Expand all | Expand 10 after
1842 0xafff, // 0x30 - 0x3f 1111111111110101 1959 0xafff, // 0x30 - 0x3f 1111111111110101
1843 // @ABCDEFGHIJKLMNO 1960 // @ABCDEFGHIJKLMNO
1844 0xffff, // 0x40 - 0x4f 1111111111111111 1961 0xffff, // 0x40 - 0x4f 1111111111111111
1845 // PQRSTUVWXYZ _ 1962 // PQRSTUVWXYZ _
1846 0x87ff, // 0x50 - 0x5f 1111111111100001 1963 0x87ff, // 0x50 - 0x5f 1111111111100001
1847 // abcdefghijklmno 1964 // abcdefghijklmno
1848 0xfffe, // 0x60 - 0x6f 0111111111111111 1965 0xfffe, // 0x60 - 0x6f 0111111111111111
1849 // pqrstuvwxyz ~ 1966 // pqrstuvwxyz ~
1850 0x47ff]; // 0x70 - 0x7f 1111111111100010 1967 0x47ff]; // 0x70 - 0x7f 1111111111100010
1851 } 1968 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698