diff --git a/headers/private/shared/JsonTextWriter.h b/headers/private/shared/JsonTextWriter.h index a4e57f3d66..1969ede908 100644 --- a/headers/private/shared/JsonTextWriter.h +++ b/headers/private/shared/JsonTextWriter.h @@ -39,6 +39,7 @@ private: status_t StreamStringEncoded(const char* string); status_t StreamStringEncoded(const char* string, off_t offset, size_t length); + status_t StreamStringUnicodeCharacter(uint32 c); status_t StreamQuotedEncodedString(const char* string); status_t StreamQuotedEncodedString(const char* string, diff --git a/src/kits/shared/JsonTextWriter.cpp b/src/kits/shared/JsonTextWriter.cpp index 35c95122ff..fa07d1f876 100644 --- a/src/kits/shared/JsonTextWriter.cpp +++ b/src/kits/shared/JsonTextWriter.cpp @@ -55,6 +55,20 @@ b_json_simple_esc_sequence(char c) } +static size_t +b_json_len_7bit_clean_non_esc(uint8* c, size_t length) { + size_t result = 0; + + while (result < length + && b_json_is_7bit_clean(c[result]) + && b_json_simple_esc_sequence(c[result]) == NULL) { + result++; + } + + return result; +} + + /*! The class and sub-classes of it are used as a stack internal to the BJsonTextWriter class. As the JSON is parsed, the stack of these internal listeners follows the stack of the JSON parsing in terms of @@ -595,6 +609,16 @@ BJsonTextWriter::StreamStringEncoded(const char* string) } +status_t +BJsonTextWriter::StreamStringUnicodeCharacter(uint32 c) +{ + sprintf(&fUnicodeAssemblyBuffer[2], "%04" B_PRIx32, c); + // note that the buffer's first two bytes are populated with the JSON + // prefix for a unicode char. + return StreamStringVerbatim(fUnicodeAssemblyBuffer, 0, 6); +} + + /*! Note that this method will expect a UTF-8 encoded string. */ status_t @@ -603,67 +627,43 @@ BJsonTextWriter::StreamStringEncoded(const char* string, { status_t writeResult = B_OK; uint8* string8bit = (uint8*)string; + off_t i = 0; - while (writeResult == B_OK && length != 0) { - uint8 c = string8bit[offset]; + while (i < length && writeResult == B_OK) { + uint8 c = string8bit[offset + i]; const char* simpleEsc = b_json_simple_esc_sequence(c); - // simple escape sequence involving the backslash + one character. - if (simpleEsc != NULL) { + // here the character to emit is something like a tab or a quote + // in this case the output JSON should escape it so that it looks + // like \t or \n in the output. writeResult = StreamStringVerbatim(simpleEsc, 0, 2); - - if (writeResult == B_OK) { - offset++; - length--; - } + i++; } else { - if (b_json_is_7bit_clean(c)) { - - // roll forward while the characters are simple and then - // output them at as a block verbatim. - - uint32 count7BitClean = 1; - - while (count7BitClean < length - && b_json_is_7bit_clean( - string8bit[offset + count7BitClean])) { - count7BitClean++; - } - - writeResult = StreamStringVerbatim(&string[offset], 0, - count7BitClean); - - if (writeResult == B_OK) { - offset += count7BitClean; - length -= count7BitClean; - } + // in this case the first character is a simple one that can be + // output without any special handling. Find the sequence of + // such characters and output them as a sequence so that it's + // included as one write operation. + size_t l = 1 + b_json_len_7bit_clean_non_esc( + &string8bit[offset + i + 1], length - (offset + i + 1)); + writeResult = StreamStringVerbatim(&string[offset + i], 0, l); + i += static_cast(l); } else { if (b_json_is_illegal(c)) { fprintf(stderr, "! string encoding error - illegal " "character [%" B_PRIu32 "]\n", static_cast(c)); - offset++; - length--; + i++; } else { - // if the character is < 128 then it can be rendered - // verbatim - check how many are like this and then - // render those verbatim. - const char* stringInitial = &string[offset]; + // now we have a UTF-8 sequence. Read the UTF-8 sequence + // to get the unicode character and then encode that as + // JSON. + const char* unicodeStr = &string[offset + i]; uint32 unicodeCharacter = BUnicodeChar::FromUTF8( - &stringInitial); - - sprintf(&fUnicodeAssemblyBuffer[2], "%04" B_PRIx32, + &unicodeStr); + writeResult = StreamStringUnicodeCharacter( unicodeCharacter); - writeResult = StreamStringVerbatim(fUnicodeAssemblyBuffer, - 0, 6); - - if (writeResult == B_OK) { - uint32 sequence_length - = (uint32)(stringInitial - &string[offset]); - offset += sequence_length; - length -= sequence_length; - } + i += static_cast(unicodeStr - &string[offset + i]); } } } diff --git a/src/tests/kits/shared/JsonTextWriterTest.cpp b/src/tests/kits/shared/JsonTextWriterTest.cpp index 91585f9fe2..c65045c1dc 100644 --- a/src/tests/kits/shared/JsonTextWriterTest.cpp +++ b/src/tests/kits/shared/JsonTextWriterTest.cpp @@ -173,6 +173,52 @@ JsonTextWriterTest::TestFalse() } +void +JsonTextWriterTest::TestStringGeneric(const char *input, + const char *expectedOut) +{ + BMallocIO* outputData = new BMallocIO(); + ObjectDeleter outputDataDeleter(outputData); + BJsonTextWriter writer(outputData); + + CPPUNIT_ASSERT_EQUAL(B_OK, writer.WriteString(input)); + writer.Complete(); + + BString outputString((char*)outputData->Buffer(), + outputData->BufferLength()); + fprintf(stderr, "expected out >%s<\n", expectedOut); + fprintf(stderr, "actual out >%s< (%" B_PRIuSIZE ")\n", + outputString.String(), outputData->BufferLength()); + + CPPUNIT_ASSERT_EQUAL(BString(expectedOut), + outputString); +} + +void +JsonTextWriterTest::TestString() +{ + TestStringGeneric( + "\"Eichh\xc3\xb6rnchen\"\nsind\nTiere.", + "\"\\\"Eichh\\u00f6rnchen\\\"\\nsind\\nTiere.\""); + // complex example with unicode, escapes and simple sequences. + TestStringGeneric("", "\"\""); + TestStringGeneric("Press \"C\" to continue", + "\"Press \\\"C\\\" to continue\""); + // test of a simple string of one character enclosed with escape + // characters to check handling of one character simple sub-sequences. + TestStringGeneric("\xc3\xb6", "\"\\u00f6\""); + // test of a unicode character on its own. + TestStringGeneric("simple", "\"simple\""); + // test of a simple string that contains no escapes or anything complex. + TestStringGeneric("\t", "\"\\t\""); + // test of a single escape character. + TestStringGeneric("\007B", "\"B\""); + // contains an illegal character which should be ignored. + TestStringGeneric("X", "\"X\""); + // a simple string with a single character +} + + /*static*/ void JsonTextWriterTest::AddTests(BTestSuite& parent) { @@ -191,6 +237,10 @@ JsonTextWriterTest::AddTests(BTestSuite& parent) "JsonTextWriterTest::TestFalse", &JsonTextWriterTest::TestFalse)); + suite.addTest(new CppUnit::TestCaller( + "JsonTextWriterTest::TestString", + &JsonTextWriterTest::TestString)); + suite.addTest(new CppUnit::TestCaller( "JsonTextWriterTest::TestArrayA", &JsonTextWriterTest::TestArrayA)); diff --git a/src/tests/kits/shared/JsonTextWriterTest.h b/src/tests/kits/shared/JsonTextWriterTest.h index 1f47003f9d..4ab4b8cd73 100644 --- a/src/tests/kits/shared/JsonTextWriterTest.h +++ b/src/tests/kits/shared/JsonTextWriterTest.h @@ -25,9 +25,12 @@ public: void TestDouble(); void TestInteger(); void TestFalse(); + void TestString(); static void AddTests(BTestSuite& suite); private: + void TestStringGeneric(const char *input, + const char *expectedOut); };