🚧 overworked error handlers #1198

This commit is contained in:
Niels Lohmann 2018-10-21 11:49:37 +02:00
parent 0671e92ced
commit c5821d91e5
No known key found for this signature in database
GPG Key ID: 7F3CEA63AE251B69
4 changed files with 181 additions and 66 deletions

View File

@ -28,6 +28,14 @@ namespace detail
// serialization // // serialization //
/////////////////// ///////////////////
/// how to treat decoding errors
enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
};
template<typename BasicJsonType> template<typename BasicJsonType>
class serializer class serializer
{ {
@ -39,23 +47,20 @@ class serializer
static constexpr uint8_t UTF8_REJECT = 1; static constexpr uint8_t UTF8_REJECT = 1;
public: public:
/// how to treat decoding errors
enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
};
/*! /*!
@param[in] s output stream to serialize to @param[in] s output stream to serialize to
@param[in] ichar indentation character to use @param[in] ichar indentation character to use
@param[in] error_handler_ how to react on decoding errors
*/ */
serializer(output_adapter_t<char> s, const char ichar) serializer(output_adapter_t<char> s, const char ichar,
: o(std::move(s)), loc(std::localeconv()), error_handler_t error_handler_ = error_handler_t::strict)
thousands_sep(loc->thousands_sep == nullptr ? '\0' : * (loc->thousands_sep)), : o(std::move(s))
decimal_point(loc->decimal_point == nullptr ? '\0' : * (loc->decimal_point)), , loc(std::localeconv())
indent_char(ichar), indent_string(512, indent_char) , thousands_sep(loc->thousands_sep == nullptr ? '\0' : * (loc->thousands_sep))
, decimal_point(loc->decimal_point == nullptr ? '\0' : * (loc->decimal_point))
, indent_char(ichar)
, indent_string(512, indent_char)
, error_handler(error_handler_)
{} {}
// delete because of pointer members // delete because of pointer members
@ -286,17 +291,18 @@ class serializer
@param[in] s the string to escape @param[in] s the string to escape
@param[in] ensure_ascii whether to escape non-ASCII characters with @param[in] ensure_ascii whether to escape non-ASCII characters with
\uXXXX sequences \uXXXX sequences
@param[in] error_handler how to react on decoding errors
@complexity Linear in the length of string @a s. @complexity Linear in the length of string @a s.
*/ */
void dump_escaped(const string_t& s, const bool ensure_ascii, void dump_escaped(const string_t& s, const bool ensure_ascii)
const error_handler_t error_handler = error_handler_t::strict)
{ {
uint32_t codepoint; uint32_t codepoint;
uint8_t state = UTF8_ACCEPT; uint8_t state = UTF8_ACCEPT;
std::size_t bytes = 0; // number of bytes written to string_buffer std::size_t bytes = 0; // number of bytes written to string_buffer
// number of bytes written at the point of the last valid byte
std::size_t bytes_after_last_accept = 0;
for (std::size_t i = 0; i < s.size(); ++i) for (std::size_t i = 0; i < s.size(); ++i)
{ {
const auto byte = static_cast<uint8_t>(s[i]); const auto byte = static_cast<uint8_t>(s[i]);
@ -394,6 +400,9 @@ class serializer
o->write_characters(string_buffer.data(), bytes); o->write_characters(string_buffer.data(), bytes);
bytes = 0; bytes = 0;
} }
// remember the byte position of this accept
bytes_after_last_accept = bytes;
break; break;
} }
@ -409,19 +418,33 @@ class serializer
} }
case error_handler_t::ignore: case error_handler_t::ignore:
{
state = UTF8_ACCEPT;
continue;
}
case error_handler_t::replace: case error_handler_t::replace:
{ {
string_buffer[bytes++] = '\\'; // in case we saw this chatacter the first time, we
string_buffer[bytes++] = 'u'; // would like to read it again, because the byte
string_buffer[bytes++] = 'f'; // may be OK for itself, but just not OK for the
string_buffer[bytes++] = 'f'; // previous sequence
string_buffer[bytes++] = 'f'; if (bytes_after_last_accept != bytes)
string_buffer[bytes++] = 'd'; {
--i;
}
// reset length buffer to the last accepted index;
// thus removing/ignoring the invalid characters
bytes = bytes_after_last_accept;
if (error_handler == error_handler_t::replace)
{
// add a replacement character
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'u';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'd';
}
// continue processing the string
state = UTF8_ACCEPT; state = UTF8_ACCEPT;
continue; continue;
} }
@ -440,6 +463,7 @@ class serializer
} }
} }
// we finished processing the string
if (JSON_LIKELY(state == UTF8_ACCEPT)) if (JSON_LIKELY(state == UTF8_ACCEPT))
{ {
// write buffer // write buffer
@ -462,13 +486,16 @@ class serializer
case error_handler_t::ignore: case error_handler_t::ignore:
{ {
// write all accepted bytes
o->write_characters(string_buffer.data(), bytes_after_last_accept);
break; break;
} }
case error_handler_t::replace: case error_handler_t::replace:
{ {
// write buffer, but replace last byte // write all accepted bytes
o->write_characters(string_buffer.data(), bytes - 1); o->write_characters(string_buffer.data(), bytes_after_last_accept);
// add a replacement character
o->write_characters("\\ufffd", 6); o->write_characters("\\ufffd", 6);
break; break;
} }
@ -682,6 +709,9 @@ class serializer
const char indent_char; const char indent_char;
/// the indentation string /// the indentation string
string_t indent_string; string_t indent_string;
/// error_handler how to react on decoding errors
const error_handler_t error_handler;
}; };
} // namespace detail } // namespace detail
} // namespace nlohmann } // namespace nlohmann

View File

@ -208,6 +208,8 @@ class basic_json
using json_pointer = ::nlohmann::json_pointer<basic_json>; using json_pointer = ::nlohmann::json_pointer<basic_json>;
template<typename T, typename SFINAE> template<typename T, typename SFINAE>
using json_serializer = JSONSerializer<T, SFINAE>; using json_serializer = JSONSerializer<T, SFINAE>;
/// how to treat decoding errors
using error_handler_t = detail::error_handler_t;
/// helper type for initializer lists of basic_json values /// helper type for initializer lists of basic_json values
using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>; using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
@ -1932,6 +1934,10 @@ class basic_json
@param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters
in the output are escaped with `\uXXXX` sequences, and the result consists in the output are escaped with `\uXXXX` sequences, and the result consists
of ASCII characters only. of ASCII characters only.
@param[in] error_handler how to react on decoding errors; there are three
possible values: `strict` (throws and exception in case a decoding error
occurs; default), `replace` (replace invalid UTF-8 sequences with U+FFFD),
and `ignore` (ignore invalid UTF-8 sequences during serialization).
@return string containing the serialization of the JSON value @return string containing the serialization of the JSON value
@ -1950,13 +1956,16 @@ class basic_json
@see https://docs.python.org/2/library/json.html#json.dump @see https://docs.python.org/2/library/json.html#json.dump
@since version 1.0.0; indentation character @a indent_char, option @since version 1.0.0; indentation character @a indent_char, option
@a ensure_ascii and exceptions added in version 3.0.0 @a ensure_ascii and exceptions added in version 3.0.0; error
handlers added in version 3.4.0.
*/ */
string_t dump(const int indent = -1, const char indent_char = ' ', string_t dump(const int indent = -1,
const bool ensure_ascii = false) const const char indent_char = ' ',
const bool ensure_ascii = false,
const error_handler_t error_handler = error_handler_t::strict) const
{ {
string_t result; string_t result;
serializer s(detail::output_adapter<char, string_t>(result), indent_char); serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);
if (indent >= 0) if (indent >= 0)
{ {

View File

@ -9980,6 +9980,14 @@ namespace detail
// serialization // // serialization //
/////////////////// ///////////////////
/// how to treat decoding errors
enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
};
template<typename BasicJsonType> template<typename BasicJsonType>
class serializer class serializer
{ {
@ -9991,23 +9999,20 @@ class serializer
static constexpr uint8_t UTF8_REJECT = 1; static constexpr uint8_t UTF8_REJECT = 1;
public: public:
/// how to treat decoding errors
enum class error_handler_t
{
strict, ///< throw a type_error exception in case of invalid UTF-8
replace, ///< replace invalid UTF-8 sequences with U+FFFD
ignore ///< ignore invalid UTF-8 sequences
};
/*! /*!
@param[in] s output stream to serialize to @param[in] s output stream to serialize to
@param[in] ichar indentation character to use @param[in] ichar indentation character to use
@param[in] error_handler_ how to react on decoding errors
*/ */
serializer(output_adapter_t<char> s, const char ichar) serializer(output_adapter_t<char> s, const char ichar,
: o(std::move(s)), loc(std::localeconv()), error_handler_t error_handler_ = error_handler_t::strict)
thousands_sep(loc->thousands_sep == nullptr ? '\0' : * (loc->thousands_sep)), : o(std::move(s))
decimal_point(loc->decimal_point == nullptr ? '\0' : * (loc->decimal_point)), , loc(std::localeconv())
indent_char(ichar), indent_string(512, indent_char) , thousands_sep(loc->thousands_sep == nullptr ? '\0' : * (loc->thousands_sep))
, decimal_point(loc->decimal_point == nullptr ? '\0' : * (loc->decimal_point))
, indent_char(ichar)
, indent_string(512, indent_char)
, error_handler(error_handler_)
{} {}
// delete because of pointer members // delete because of pointer members
@ -10238,17 +10243,18 @@ class serializer
@param[in] s the string to escape @param[in] s the string to escape
@param[in] ensure_ascii whether to escape non-ASCII characters with @param[in] ensure_ascii whether to escape non-ASCII characters with
\uXXXX sequences \uXXXX sequences
@param[in] error_handler how to react on decoding errors
@complexity Linear in the length of string @a s. @complexity Linear in the length of string @a s.
*/ */
void dump_escaped(const string_t& s, const bool ensure_ascii, void dump_escaped(const string_t& s, const bool ensure_ascii)
const error_handler_t error_handler = error_handler_t::strict)
{ {
uint32_t codepoint; uint32_t codepoint;
uint8_t state = UTF8_ACCEPT; uint8_t state = UTF8_ACCEPT;
std::size_t bytes = 0; // number of bytes written to string_buffer std::size_t bytes = 0; // number of bytes written to string_buffer
// number of bytes written at the point of the last valid byte
std::size_t bytes_after_last_accept = 0;
for (std::size_t i = 0; i < s.size(); ++i) for (std::size_t i = 0; i < s.size(); ++i)
{ {
const auto byte = static_cast<uint8_t>(s[i]); const auto byte = static_cast<uint8_t>(s[i]);
@ -10346,6 +10352,9 @@ class serializer
o->write_characters(string_buffer.data(), bytes); o->write_characters(string_buffer.data(), bytes);
bytes = 0; bytes = 0;
} }
// remember the byte position of this accept
bytes_after_last_accept = bytes;
break; break;
} }
@ -10361,19 +10370,33 @@ class serializer
} }
case error_handler_t::ignore: case error_handler_t::ignore:
{
state = UTF8_ACCEPT;
continue;
}
case error_handler_t::replace: case error_handler_t::replace:
{ {
string_buffer[bytes++] = '\\'; // in case we saw this chatacter the first time, we
string_buffer[bytes++] = 'u'; // would like to read it again, because the byte
string_buffer[bytes++] = 'f'; // may be OK for itself, but just not OK for the
string_buffer[bytes++] = 'f'; // previous sequence
string_buffer[bytes++] = 'f'; if (bytes_after_last_accept != bytes)
string_buffer[bytes++] = 'd'; {
--i;
}
// reset length buffer to the last accepted index;
// thus removing/ignoring the invalid characters
bytes = bytes_after_last_accept;
if (error_handler == error_handler_t::replace)
{
// add a replacement character
string_buffer[bytes++] = '\\';
string_buffer[bytes++] = 'u';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'f';
string_buffer[bytes++] = 'd';
}
// continue processing the string
state = UTF8_ACCEPT; state = UTF8_ACCEPT;
continue; continue;
} }
@ -10392,6 +10415,7 @@ class serializer
} }
} }
// we finished processing the string
if (JSON_LIKELY(state == UTF8_ACCEPT)) if (JSON_LIKELY(state == UTF8_ACCEPT))
{ {
// write buffer // write buffer
@ -10414,13 +10438,16 @@ class serializer
case error_handler_t::ignore: case error_handler_t::ignore:
{ {
// write all accepted bytes
o->write_characters(string_buffer.data(), bytes_after_last_accept);
break; break;
} }
case error_handler_t::replace: case error_handler_t::replace:
{ {
// write buffer, but replace last byte // write all accepted bytes
o->write_characters(string_buffer.data(), bytes - 1); o->write_characters(string_buffer.data(), bytes_after_last_accept);
// add a replacement character
o->write_characters("\\ufffd", 6); o->write_characters("\\ufffd", 6);
break; break;
} }
@ -10634,6 +10661,9 @@ class serializer
const char indent_char; const char indent_char;
/// the indentation string /// the indentation string
string_t indent_string; string_t indent_string;
/// error_handler how to react on decoding errors
const error_handler_t error_handler;
}; };
} // namespace detail } // namespace detail
} // namespace nlohmann } // namespace nlohmann
@ -11603,6 +11633,8 @@ class basic_json
using json_pointer = ::nlohmann::json_pointer<basic_json>; using json_pointer = ::nlohmann::json_pointer<basic_json>;
template<typename T, typename SFINAE> template<typename T, typename SFINAE>
using json_serializer = JSONSerializer<T, SFINAE>; using json_serializer = JSONSerializer<T, SFINAE>;
/// how to treat decoding errors
using error_handler_t = detail::error_handler_t;
/// helper type for initializer lists of basic_json values /// helper type for initializer lists of basic_json values
using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>; using initializer_list_t = std::initializer_list<detail::json_ref<basic_json>>;
@ -13327,6 +13359,10 @@ class basic_json
@param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters
in the output are escaped with `\uXXXX` sequences, and the result consists in the output are escaped with `\uXXXX` sequences, and the result consists
of ASCII characters only. of ASCII characters only.
@param[in] error_handler how to react on decoding errors; there are three
possible values: `strict` (throws and exception in case a decoding error
occurs; default), `replace` (replace invalid UTF-8 sequences with U+FFFD),
and `ignore` (ignore invalid UTF-8 sequences during serialization).
@return string containing the serialization of the JSON value @return string containing the serialization of the JSON value
@ -13345,13 +13381,16 @@ class basic_json
@see https://docs.python.org/2/library/json.html#json.dump @see https://docs.python.org/2/library/json.html#json.dump
@since version 1.0.0; indentation character @a indent_char, option @since version 1.0.0; indentation character @a indent_char, option
@a ensure_ascii and exceptions added in version 3.0.0 @a ensure_ascii and exceptions added in version 3.0.0; error
handlers added in version 3.4.0.
*/ */
string_t dump(const int indent = -1, const char indent_char = ' ', string_t dump(const int indent = -1,
const bool ensure_ascii = false) const const char indent_char = ' ',
const bool ensure_ascii = false,
const error_handler_t error_handler = error_handler_t::strict) const
{ {
string_t result; string_t result;
serializer s(detail::output_adapter<char, string_t>(result), indent_char); serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);
if (indent >= 0) if (indent >= 0)
{ {

View File

@ -94,4 +94,41 @@ TEST_CASE("serialization")
"[\n\t\"foo\",\n\t1,\n\t2,\n\t3,\n\tfalse,\n\t{\n\t\t\"one\": 1\n\t}\n]"); "[\n\t\"foo\",\n\t1,\n\t2,\n\t3,\n\tfalse,\n\t{\n\t\t\"one\": 1\n\t}\n]");
} }
} }
SECTION("dump")
{
SECTION("invalid character")
{
json j = "ä\xA9ü";
CHECK_THROWS_AS(j.dump(), json::type_error&);
CHECK_THROWS_WITH(j.dump(), "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9");
CHECK_THROWS_AS(j.dump(1, ' ', false, json::error_handler_t::strict), json::type_error&);
CHECK_THROWS_WITH(j.dump(1, ' ', false, json::error_handler_t::strict), "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"äü\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"ä\\ufffdü\"");
}
SECTION("ending with incomplete character")
{
json j = "123\xC2";
CHECK_THROWS_AS(j.dump(), json::type_error&);
CHECK_THROWS_WITH(j.dump(), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2");
CHECK_THROWS_AS(j.dump(1, ' ', false, json::error_handler_t::strict), json::type_error&);
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\\ufffd\"");
}
SECTION("unexpected character")
{
json j = "123\xF1\xB0\x34\x35\x36";
CHECK_THROWS_AS(j.dump(), json::type_error&);
CHECK_THROWS_WITH(j.dump(), "[json.exception.type_error.316] invalid UTF-8 byte at index 5: 0x34");
CHECK_THROWS_AS(j.dump(1, ' ', false, json::error_handler_t::strict), json::type_error&);
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123456\"");
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\\ufffd456\"");
}
}
} }