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

Side by Side Diff: base/json/json_reader.cc

Issue 9801007: Improve JSONReader performance by up to 55% by using std::string instead of wstring. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Safety for \x Created 8 years, 9 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
« no previous file with comments | « base/json/json_reader.h ('k') | base/json/json_reader_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "base/json/json_reader.h" 5 #include "base/json/json_reader.h"
6 6
7 #include "base/float_util.h" 7 #include "base/float_util.h"
8 #include "base/logging.h" 8 #include "base/logging.h"
9 #include "base/memory/scoped_ptr.h" 9 #include "base/memory/scoped_ptr.h"
10 #include "base/stringprintf.h" 10 #include "base/stringprintf.h"
11 #include "base/string_number_conversions.h" 11 #include "base/string_number_conversions.h"
12 #include "base/string_piece.h"
12 #include "base/string_util.h" 13 #include "base/string_util.h"
14 #include "base/third_party/icu/icu_utf.h"
13 #include "base/utf_string_conversions.h" 15 #include "base/utf_string_conversions.h"
14 #include "base/values.h" 16 #include "base/values.h"
15 17
16 namespace { 18 namespace {
17 19
18 const wchar_t kNullString[] = L"null"; 20 const char kNullString[] = "null";
19 const wchar_t kTrueString[] = L"true"; 21 const char kTrueString[] = "true";
20 const wchar_t kFalseString[] = L"false"; 22 const char kFalseString[] = "false";
21 23
22 const int kStackLimit = 100; 24 const int kStackLimit = 100;
23 25
24 // A helper method for ParseNumberToken. It reads an int from the end of 26 // A helper method for ParseNumberToken. It reads an int from the end of
25 // token. The method returns false if there is no valid integer at the end of 27 // token. The method returns false if there is no valid integer at the end of
26 // the token. 28 // the token.
27 bool ReadInt(base::JSONReader::Token& token, bool can_have_leading_zeros) { 29 bool ReadInt(base::JSONReader::Token& token, bool can_have_leading_zeros) {
28 wchar_t first = token.NextChar(); 30 char first = token.NextChar();
29 int len = 0; 31 int len = 0;
30 32
31 // Read in more digits. 33 // Read in more digits.
32 wchar_t c = first; 34 char c = first;
33 while ('\0' != c && IsAsciiDigit(c)) { 35 while ('\0' != c && IsAsciiDigit(c)) {
34 ++token.length; 36 ++token.length;
35 ++len; 37 ++len;
36 c = token.NextChar(); 38 c = token.NextChar();
37 } 39 }
38 // We need at least 1 digit. 40 // We need at least 1 digit.
39 if (len == 0) 41 if (len == 0)
40 return false; 42 return false;
41 43
42 if (!can_have_leading_zeros && len > 1 && '0' == first) 44 if (!can_have_leading_zeros && len > 1 && '0' == first)
43 return false; 45 return false;
44 46
45 return true; 47 return true;
46 } 48 }
47 49
48 // A helper method for ParseStringToken. It reads |digits| hex digits from the 50 // A helper method for ParseStringToken. It reads |digits| hex digits from the
49 // token. If the sequence if digits is not valid (contains other characters), 51 // token. If the sequence if digits is not valid (contains other characters),
50 // the method returns false. 52 // the method returns false.
51 bool ReadHexDigits(base::JSONReader::Token& token, int digits) { 53 bool ReadHexDigits(base::JSONReader::Token& token, int digits) {
52 for (int i = 1; i <= digits; ++i) { 54 for (int i = 1; i <= digits; ++i) {
53 wchar_t c = *(token.begin + token.length + i); 55 char c = *(token.begin + token.length + i);
54 if (c == '\0' || !IsHexDigit(c)) 56 if (c == '\0' || !IsHexDigit(c))
55 return false; 57 return false;
56 } 58 }
57 59
58 token.length += digits; 60 token.length += digits;
59 return true; 61 return true;
60 } 62 }
61 63
62 } // namespace 64 } // namespace
63 65
(...skipping 12 matching lines...) Expand all
76 const char* JSONReader::kUnexpectedDataAfterRoot = 78 const char* JSONReader::kUnexpectedDataAfterRoot =
77 "Unexpected data after root element."; 79 "Unexpected data after root element.";
78 const char* JSONReader::kUnsupportedEncoding = 80 const char* JSONReader::kUnsupportedEncoding =
79 "Unsupported encoding. JSON must be UTF-8."; 81 "Unsupported encoding. JSON must be UTF-8.";
80 const char* JSONReader::kUnquotedDictionaryKey = 82 const char* JSONReader::kUnquotedDictionaryKey =
81 "Dictionary keys must be quoted."; 83 "Dictionary keys must be quoted.";
82 84
83 JSONReader::JSONReader() 85 JSONReader::JSONReader()
84 : start_pos_(NULL), 86 : start_pos_(NULL),
85 json_pos_(NULL), 87 json_pos_(NULL),
88 end_pos_(NULL),
86 stack_depth_(0), 89 stack_depth_(0),
87 allow_trailing_comma_(false), 90 allow_trailing_comma_(false),
88 error_code_(JSON_NO_ERROR), 91 error_code_(JSON_NO_ERROR),
89 error_line_(0), 92 error_line_(0),
90 error_col_(0) {} 93 error_col_(0) {}
91 94
92 // static 95 // static
93 Value* JSONReader::Read(const std::string& json, 96 Value* JSONReader::Read(const std::string& json,
94 bool allow_trailing_comma) { 97 bool allow_trailing_comma) {
95 return ReadAndReturnError(json, allow_trailing_comma, NULL, NULL); 98 return ReadAndReturnError(json, allow_trailing_comma, NULL, NULL);
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
141 } 144 }
142 145
143 std::string JSONReader::GetErrorMessage() const { 146 std::string JSONReader::GetErrorMessage() const {
144 return FormatErrorMessage(error_line_, error_col_, 147 return FormatErrorMessage(error_line_, error_col_,
145 ErrorCodeToString(error_code_)); 148 ErrorCodeToString(error_code_));
146 } 149 }
147 150
148 Value* JSONReader::JsonToValue(const std::string& json, bool check_root, 151 Value* JSONReader::JsonToValue(const std::string& json, bool check_root,
149 bool allow_trailing_comma) { 152 bool allow_trailing_comma) {
150 // The input must be in UTF-8. 153 // The input must be in UTF-8.
151 if (!IsStringUTF8(json.c_str())) { 154 if (!IsStringUTF8(json.data())) {
152 error_code_ = JSON_UNSUPPORTED_ENCODING; 155 error_code_ = JSON_UNSUPPORTED_ENCODING;
153 return NULL; 156 return NULL;
154 } 157 }
155 158
156 // The conversion from UTF8 to wstring removes null bytes for us 159 start_pos_ = json.data();
157 // (a good thing). 160 end_pos_ = start_pos_ + json.size();
158 std::wstring json_wide(UTF8ToWide(json));
159 start_pos_ = json_wide.c_str();
160 161
161 // When the input JSON string starts with a UTF-8 Byte-Order-Mark 162 // When the input JSON string starts with a UTF-8 Byte-Order-Mark (U+FEFF)
162 // (0xEF, 0xBB, 0xBF), the UTF8ToWide() function converts it to a Unicode 163 // or <0xEF 0xBB 0xBF>, advance the start position to avoid the
163 // BOM (U+FEFF). To avoid the JSONReader::BuildValue() function from 164 // JSONReader::BuildValue() function from mis-treating a Unicode BOM as an
164 // mis-treating a Unicode BOM as an invalid character and returning NULL, 165 // invalid character and returning NULL.
165 // skip a converted Unicode BOM if it exists. 166 if (json.size() >= 3 && start_pos_[0] == 0xEF &&
166 if (!json_wide.empty() && start_pos_[0] == 0xFEFF) { 167 start_pos_[1] == 0xBB && start_pos_[2] == 0xBF) {
167 ++start_pos_; 168 start_pos_ += 3;
168 } 169 }
169 170
170 json_pos_ = start_pos_; 171 json_pos_ = start_pos_;
171 allow_trailing_comma_ = allow_trailing_comma; 172 allow_trailing_comma_ = allow_trailing_comma;
172 stack_depth_ = 0; 173 stack_depth_ = 0;
173 error_code_ = JSON_NO_ERROR; 174 error_code_ = JSON_NO_ERROR;
174 175
175 scoped_ptr<Value> root(BuildValue(check_root)); 176 scoped_ptr<Value> root(BuildValue(check_root));
176 if (root.get()) { 177 if (root.get()) {
177 if (ParseToken().type == Token::END_OF_INPUT) { 178 if (ParseToken().type == Token::END_OF_INPUT) {
(...skipping 171 matching lines...) Expand 10 before | Expand all | Expand 10 after
349 json_pos_ += token.length; 350 json_pos_ += token.length;
350 351
351 --stack_depth_; 352 --stack_depth_;
352 return node.release(); 353 return node.release();
353 } 354 }
354 355
355 JSONReader::Token JSONReader::ParseNumberToken() { 356 JSONReader::Token JSONReader::ParseNumberToken() {
356 // We just grab the number here. We validate the size in DecodeNumber. 357 // We just grab the number here. We validate the size in DecodeNumber.
357 // According to RFC4627, a valid number is: [minus] int [frac] [exp] 358 // According to RFC4627, a valid number is: [minus] int [frac] [exp]
358 Token token(Token::NUMBER, json_pos_, 0); 359 Token token(Token::NUMBER, json_pos_, 0);
359 wchar_t c = *json_pos_; 360 char c = *json_pos_;
360 if ('-' == c) { 361 if ('-' == c) {
361 ++token.length; 362 ++token.length;
362 c = token.NextChar(); 363 c = token.NextChar();
363 } 364 }
364 365
365 if (!ReadInt(token, false)) 366 if (!ReadInt(token, false))
366 return Token::CreateInvalidToken(); 367 return Token::CreateInvalidToken();
367 368
368 // Optional fraction part 369 // Optional fraction part
369 c = token.NextChar(); 370 c = token.NextChar();
(...skipping 13 matching lines...) Expand all
383 c = token.NextChar(); 384 c = token.NextChar();
384 } 385 }
385 if (!ReadInt(token, true)) 386 if (!ReadInt(token, true))
386 return Token::CreateInvalidToken(); 387 return Token::CreateInvalidToken();
387 } 388 }
388 389
389 return token; 390 return token;
390 } 391 }
391 392
392 Value* JSONReader::DecodeNumber(const Token& token) { 393 Value* JSONReader::DecodeNumber(const Token& token) {
393 const std::wstring num_string(token.begin, token.length); 394 const std::string num_string(token.begin, token.length);
394 395
395 int num_int; 396 int num_int;
396 if (StringToInt(WideToUTF8(num_string), &num_int)) 397 if (StringToInt(num_string, &num_int))
397 return Value::CreateIntegerValue(num_int); 398 return Value::CreateIntegerValue(num_int);
398 399
399 double num_double; 400 double num_double;
400 if (StringToDouble(WideToUTF8(num_string), &num_double) && 401 if (StringToDouble(num_string, &num_double) && base::IsFinite(num_double))
401 base::IsFinite(num_double))
402 return Value::CreateDoubleValue(num_double); 402 return Value::CreateDoubleValue(num_double);
403 403
404 return NULL; 404 return NULL;
405 } 405 }
406 406
407 JSONReader::Token JSONReader::ParseStringToken() { 407 JSONReader::Token JSONReader::ParseStringToken() {
408 Token token(Token::STRING, json_pos_, 1); 408 Token token(Token::STRING, json_pos_, 1);
409 wchar_t c = token.NextChar(); 409 char c = token.NextChar();
410 while ('\0' != c) { 410 while (json_pos_ + token.length < end_pos_) {
411 if ('\\' == c) { 411 if ('\\' == c) {
412 ++token.length; 412 ++token.length;
413 c = token.NextChar(); 413 c = token.NextChar();
414 // Make sure the escaped char is valid. 414 // Make sure the escaped char is valid.
415 switch (c) { 415 switch (c) {
416 case 'x': 416 case 'x':
417 if (!ReadHexDigits(token, 2)) { 417 if (!ReadHexDigits(token, 2)) {
418 SetErrorCode(JSON_INVALID_ESCAPE, json_pos_ + token.length); 418 SetErrorCode(JSON_INVALID_ESCAPE, json_pos_ + token.length);
419 return Token::CreateInvalidToken(); 419 return Token::CreateInvalidToken();
420 } 420 }
(...skipping 22 matching lines...) Expand all
443 ++token.length; 443 ++token.length;
444 return token; 444 return token;
445 } 445 }
446 ++token.length; 446 ++token.length;
447 c = token.NextChar(); 447 c = token.NextChar();
448 } 448 }
449 return Token::CreateInvalidToken(); 449 return Token::CreateInvalidToken();
450 } 450 }
451 451
452 Value* JSONReader::DecodeString(const Token& token) { 452 Value* JSONReader::DecodeString(const Token& token) {
453 std::wstring decoded_str; 453 std::string decoded_str;
454 decoded_str.reserve(token.length - 2); 454 decoded_str.reserve(token.length - 2);
455 455
456 for (int i = 1; i < token.length - 1; ++i) { 456 for (int i = 1; i < token.length - 1; ++i) {
457 wchar_t c = *(token.begin + i); 457 char c = *(token.begin + i);
458 if ('\\' == c) { 458 if ('\\' == c) {
459 ++i; 459 ++i;
460 c = *(token.begin + i); 460 c = *(token.begin + i);
461 switch (c) { 461 switch (c) {
462 case '"': 462 case '"':
463 case '/': 463 case '/':
464 case '\\': 464 case '\\':
465 decoded_str.push_back(c); 465 decoded_str.push_back(c);
466 break; 466 break;
467 case 'b': 467 case 'b':
468 decoded_str.push_back('\b'); 468 decoded_str.push_back('\b');
469 break; 469 break;
470 case 'f': 470 case 'f':
471 decoded_str.push_back('\f'); 471 decoded_str.push_back('\f');
472 break; 472 break;
473 case 'n': 473 case 'n':
474 decoded_str.push_back('\n'); 474 decoded_str.push_back('\n');
475 break; 475 break;
476 case 'r': 476 case 'r':
477 decoded_str.push_back('\r'); 477 decoded_str.push_back('\r');
478 break; 478 break;
479 case 't': 479 case 't':
480 decoded_str.push_back('\t'); 480 decoded_str.push_back('\t');
481 break; 481 break;
482 case 'v': 482 case 'v':
483 decoded_str.push_back('\v'); 483 decoded_str.push_back('\v');
484 break; 484 break;
485 485
486 case 'x': 486 case 'x': {
487 decoded_str.push_back((HexDigitToInt(*(token.begin + i + 1)) << 4) + 487 if (i + 2 >= token.length)
488 HexDigitToInt(*(token.begin + i + 2))); 488 return NULL;
489 int hex_digit = 0;
490 if (!HexStringToInt(StringPiece(token.begin + i + 1, 2), &hex_digit))
491 return NULL;
492 decoded_str.push_back(hex_digit);
489 i += 2; 493 i += 2;
490 break; 494 break;
495 }
491 case 'u': 496 case 'u':
492 decoded_str.push_back((HexDigitToInt(*(token.begin + i + 1)) << 12 ) + 497 if (!ConvertUTF16Units(token, &i, &decoded_str))
493 (HexDigitToInt(*(token.begin + i + 2)) << 8) + 498 return NULL;
494 (HexDigitToInt(*(token.begin + i + 3)) << 4) +
495 HexDigitToInt(*(token.begin + i + 4)));
496 i += 4;
497 break; 499 break;
498 500
499 default: 501 default:
500 // We should only have valid strings at this point. If not, 502 // We should only have valid strings at this point. If not,
501 // ParseStringToken didn't do it's job. 503 // ParseStringToken didn't do it's job.
502 NOTREACHED(); 504 NOTREACHED();
503 return NULL; 505 return NULL;
504 } 506 }
505 } else { 507 } else {
506 // Not escaped 508 // Not escaped
507 decoded_str.push_back(c); 509 decoded_str.push_back(c);
508 } 510 }
509 } 511 }
510 return Value::CreateStringValue(WideToUTF16Hack(decoded_str)); 512 return Value::CreateStringValue(decoded_str);
513 }
514
515 bool JSONReader::ConvertUTF16Units(const Token& token,
516 int* i,
517 std::string* dest_string) {
518 if (*i + 4 >= token.length)
519 return false;
520
521 // This is a 32-bit field because the shift operations in the
522 // conversion process below cause MSVC to error about "data loss."
523 // This only stores UTF-16 code units, though.
524 // Consume the UTF-16 code unit, which may be a high surrogate.
525 int code_unit16_high = 0;
526 if (!HexStringToInt(StringPiece(token.begin + *i + 1, 4), &code_unit16_high))
527 return false;
528 *i += 4;
529
530 // If this is a high surrogate, consume the next code unit to get the
531 // low surrogate.
532 int code_unit16_low = 0;
533 if (CBU16_IS_SURROGATE(code_unit16_high)) {
534 // Make sure this is the high surrogate. If not, it's an encoding
535 // error.
536 if (!CBU16_IS_SURROGATE_LEAD(code_unit16_high))
537 return false;
538
539 // Make sure that the token has more characters to consume the
540 // lower surrogate.
541 if (*i + 6 >= token.length)
542 return false;
543 if (*(++(*i) + token.begin) != '\\' || *(++(*i) + token.begin) != 'u')
544 return false;
545
546 if (!HexStringToInt(StringPiece(token.begin + *i + 1, 4), &code_unit16_low))
547 return false;
548 *i += 4;
549 if (!CBU16_IS_SURROGATE(code_unit16_low) ||
550 !CBU16_IS_TRAIL(code_unit16_low)) {
551 return false;
552 }
553 } else if (!CBU16_IS_SINGLE(code_unit16_high)) {
554 // If this is not a code point, it's an encoding error.
555 return false;
556 }
557
558 // Convert the UTF-16 code units to a code point and then to a UTF-8
559 // code unit sequence.
560 char code_point[8] = { 0 };
561 size_t offset = 0;
562 if (!code_unit16_low) {
563 CBU8_APPEND_UNSAFE(code_point, offset, code_unit16_high);
564 } else {
565 uint32 code_unit32 = CBU16_GET_SUPPLEMENTARY(code_unit16_high,
566 code_unit16_low);
567 offset = 0;
568 CBU8_APPEND_UNSAFE(code_point, offset, code_unit32);
569 }
570 dest_string->append(code_point);
571 return true;
511 } 572 }
512 573
513 JSONReader::Token JSONReader::ParseToken() { 574 JSONReader::Token JSONReader::ParseToken() {
514 EatWhitespaceAndComments(); 575 EatWhitespaceAndComments();
515 576
516 Token token(Token::INVALID_TOKEN, 0, 0); 577 Token token(Token::INVALID_TOKEN, 0, 0);
517 switch (*json_pos_) { 578 switch (*json_pos_) {
518 case '\0': 579 case '\0':
519 token.type = Token::END_OF_INPUT; 580 token.type = Token::END_OF_INPUT;
520 break; 581 break;
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
573 break; 634 break;
574 635
575 case '"': 636 case '"':
576 token = ParseStringToken(); 637 token = ParseStringToken();
577 break; 638 break;
578 } 639 }
579 return token; 640 return token;
580 } 641 }
581 642
582 void JSONReader::EatWhitespaceAndComments() { 643 void JSONReader::EatWhitespaceAndComments() {
583 while ('\0' != *json_pos_) { 644 while (json_pos_ != end_pos_) {
584 switch (*json_pos_) { 645 switch (*json_pos_) {
585 case ' ': 646 case ' ':
586 case '\n': 647 case '\n':
587 case '\r': 648 case '\r':
588 case '\t': 649 case '\t':
589 ++json_pos_; 650 ++json_pos_;
590 break; 651 break;
591 case '/': 652 case '/':
592 // TODO(tc): This isn't in the RFC so it should be a parser flag. 653 // TODO(tc): This isn't in the RFC so it should be a parser flag.
593 if (!EatComment()) 654 if (!EatComment())
594 return; 655 return;
595 break; 656 break;
596 default: 657 default:
597 // Not a whitespace char, just exit. 658 // Not a whitespace char, just exit.
598 return; 659 return;
599 } 660 }
600 } 661 }
601 } 662 }
602 663
603 bool JSONReader::EatComment() { 664 bool JSONReader::EatComment() {
604 if ('/' != *json_pos_) 665 if ('/' != *json_pos_)
605 return false; 666 return false;
606 667
607 wchar_t next_char = *(json_pos_ + 1); 668 char next_char = *(json_pos_ + 1);
608 if ('/' == next_char) { 669 if ('/' == next_char) {
609 // Line comment, read until \n or \r 670 // Line comment, read until \n or \r
610 json_pos_ += 2; 671 json_pos_ += 2;
611 while ('\0' != *json_pos_) { 672 while (json_pos_ != end_pos_) {
612 switch (*json_pos_) { 673 switch (*json_pos_) {
613 case '\n': 674 case '\n':
614 case '\r': 675 case '\r':
615 ++json_pos_; 676 ++json_pos_;
616 return true; 677 return true;
617 default: 678 default:
618 ++json_pos_; 679 ++json_pos_;
619 } 680 }
620 } 681 }
621 } else if ('*' == next_char) { 682 } else if ('*' == next_char) {
622 // Block comment, read until */ 683 // Block comment, read until */
623 json_pos_ += 2; 684 json_pos_ += 2;
624 while ('\0' != *json_pos_) { 685 while (json_pos_ != end_pos_) {
625 if ('*' == *json_pos_ && '/' == *(json_pos_ + 1)) { 686 if ('*' == *json_pos_ && '/' == *(json_pos_ + 1)) {
626 json_pos_ += 2; 687 json_pos_ += 2;
627 return true; 688 return true;
628 } 689 }
629 ++json_pos_; 690 ++json_pos_;
630 } 691 }
631 } else { 692 } else {
632 return false; 693 return false;
633 } 694 }
634 return true; 695 return true;
635 } 696 }
636 697
637 bool JSONReader::NextStringMatch(const wchar_t* str, size_t length) { 698 bool JSONReader::NextStringMatch(const char* str, size_t length) {
638 return wcsncmp(json_pos_, str, length) == 0; 699 return strncmp(json_pos_, str, length) == 0;
639 } 700 }
640 701
641 void JSONReader::SetErrorCode(JsonParseError error, 702 void JSONReader::SetErrorCode(JsonParseError error,
642 const wchar_t* error_pos) { 703 const char* error_pos) {
643 int line_number = 1; 704 int line_number = 1;
644 int column_number = 1; 705 int column_number = 1;
645 706
646 // Figure out the line and column the error occured at. 707 // Figure out the line and column the error occured at.
647 for (const wchar_t* pos = start_pos_; pos != error_pos; ++pos) { 708 for (const char* pos = start_pos_; pos != error_pos; ++pos) {
648 if (*pos == '\0') { 709 if (pos > end_pos_) {
649 NOTREACHED(); 710 NOTREACHED();
650 return; 711 return;
651 } 712 }
652 713
653 if (*pos == '\n') { 714 if (*pos == '\n') {
654 ++line_number; 715 ++line_number;
655 column_number = 1; 716 column_number = 1;
656 } else { 717 } else {
657 ++column_number; 718 ++column_number;
658 } 719 }
659 } 720 }
660 721
661 error_line_ = line_number; 722 error_line_ = line_number;
662 error_col_ = column_number; 723 error_col_ = column_number;
663 error_code_ = error; 724 error_code_ = error;
664 } 725 }
665 726
666 } // namespace base 727 } // namespace base
OLDNEW
« no previous file with comments | « base/json/json_reader.h ('k') | base/json/json_reader_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698