fix: restore the character that terminates a number (#5340)

operator>> is documented to leave the stream positioned right after the
parsed value, so that concatenated JSON values can be read back to back.
That did not hold for numbers: a number is only terminated by the
character following it, and lexer::scan_number() reads that character
and calls unget() -- which is simulated and rewinds only the lexer's own
bookkeeping. input_stream_adapter consumes via sbumpc() with no matching
sungetc(), so the terminating character stayed consumed and the next
extraction started one byte too late ('1true' left the stream at 'rue').

Propagating unget() to the adapter directly does not work: next_unget
makes the following get() replay the cached character, so the terminator
would be delivered twice. Instead, restore the still-pending character
once at the end of a non-strict parse, where the input is handed back to
the caller:

- input_stream_adapter gains unget_character() (sungetc()) and advertises
  it via supports_unget, detected the same way as supports_seek.
- lexer::restore_pending_unget() turns a pending simulated unget of a
  real (non-EOF) character into a real one and clears next_unget so the
  character is not also replayed. It is a no-op for adapters that cannot
  unget, and reports failure when sungetc() fails, in which case the
  input is left as it was before.
- parser calls it on the three non-strict paths, i.e. for operator>> and
  sax_parse(strict = false).

Strict parse()/accept() are unaffected: they require the input to end
after the value, so the character is consumed by the end-of-input check
anyway. Parse error messages and reported positions are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-08-01 07:33:11 +02:00
parent 634f49bc5b
commit da7b9bdb3d
8 changed files with 387 additions and 37 deletions
+96 -3
View File
@@ -7122,6 +7122,18 @@ class input_stream_adapter
return res;
}
// Whether the adapter can return the last read character to the input so
// that subsequent reads from the underlying stream see it again.
static constexpr bool supports_unget = true;
// Move the get pointer back over the character last returned by
// get_character(). Returns whether the character was actually restored;
// sungetc() may fail if the streambuf has no putback position available.
bool unget_character()
{
return sb->sungetc() != std::char_traits<char>::eof();
}
template<class T>
std::size_t get_elements(T* dest, std::size_t count = 1)
{
@@ -7823,6 +7835,24 @@ constexpr bool input_adapter_supports_seek(std::false_type /*detected*/)
return false;
}
// Detect whether an input adapter can return the character last read to the
// input (see input_stream_adapter::supports_unget), detected like
// supports_seek above.
template<typename InputAdapterType>
using detect_supports_unget = decltype(InputAdapterType::supports_unget);
template<typename InputAdapterType>
constexpr bool input_adapter_supports_unget(std::true_type /*detected*/)
{
return InputAdapterType::supports_unget;
}
template<typename InputAdapterType>
constexpr bool input_adapter_supports_unget(std::false_type /*detected*/)
{
return false;
}
/*!
@brief lexical analysis
@@ -7844,6 +7874,11 @@ class lexer : public lexer_base<BasicJsonType>
static constexpr bool lazy_token_string =
input_adapter_supports_seek<InputAdapterType>(is_detected<detect_supports_seek, InputAdapterType> {});
/// whether a pending simulated unget can be turned into a real unget on
/// the input adapter; see input_adapter_supports_unget
static constexpr bool can_unget_input =
input_adapter_supports_unget<InputAdapterType>(is_detected<detect_supports_unget, InputAdapterType> {});
public:
using token_type = typename lexer_base<BasicJsonType>::token_type;
@@ -9154,6 +9189,25 @@ scan_number_done:
uncapture_char(std::integral_constant<bool, lazy_token_string> {});
}
/// adapter without unget support: nothing to do (see restore_pending_unget)
bool restore_pending_unget_impl(std::false_type /*can_unget*/) const noexcept
{
return false;
}
/// adapter with unget support: give back the character consumed but unread
bool restore_pending_unget_impl(std::true_type /*can_unget*/)
{
if (!next_unget || current == char_traits<char_type>::eof())
{
// nothing was consumed beyond the last token
return true;
}
next_unget = false;
return ia.unget_character();
}
/// seekable adapter: nothing was captured, so nothing to undo
void uncapture_char(std::true_type /*lazy*/) const noexcept {}
@@ -9217,6 +9271,28 @@ scan_number_done:
return position;
}
/*!
@brief turn a pending simulated unget into a real one on the input
unget() only rewinds the lexer's own bookkeeping, so the character that
terminated the last token (e.g. the character after a number) stays
consumed from the input. Callers that hand the input back to the user
afterwards - operator>> and non-strict sax_parse - call this once when
scanning is done, so that the input is positioned right after the value.
A pending unget of EOF must not be restored: EOF was never consumed. The
lexer must not read again after this call; next_unget is cleared so that
the restored character is not also replayed from @a current.
@return whether the input is positioned right after the last token; false
if the adapter cannot unget or the unget failed, in which case the
input is left as is (the pre-existing behaviour)
*/
bool restore_pending_unget()
{
return restore_pending_unget_impl(std::integral_constant<bool, can_unget_input> {});
}
/// seekable adapter: rebuild the last read token from the input on demand
const std::vector<char_type>& collect_token_chars(std::vector<char_type>& out, std::true_type /*lazy*/) const
{
@@ -13920,8 +13996,14 @@ class parser
json_sax_dom_callback_parser<BasicJsonType, InputAdapterType> sdp(result, callback, allow_exceptions, &m_lexer);
sax_parse_internal(&sdp);
if (!strict)
{
// the caller keeps using the input: position it right after
// the value by giving back the character that terminated it
m_lexer.restore_pending_unget();
}
// in strict mode, input must be completely read
if (strict && (get_token() != token_type::end_of_input))
else if (get_token() != token_type::end_of_input)
{
sdp.parse_error(m_lexer.get_position(),
m_lexer.get_token_string(),
@@ -13948,8 +14030,13 @@ class parser
json_sax_dom_parser<BasicJsonType, InputAdapterType> sdp(result, allow_exceptions, &m_lexer);
sax_parse_internal(&sdp);
if (!strict)
{
// see above
m_lexer.restore_pending_unget();
}
// in strict mode, input must be completely read
if (strict && (get_token() != token_type::end_of_input))
else if (get_token() != token_type::end_of_input)
{
sdp.parse_error(m_lexer.get_position(),
m_lexer.get_token_string(),
@@ -13986,8 +14073,14 @@ class parser
(void)detail::is_sax_static_asserts<SAX, BasicJsonType> {};
const bool result = sax_parse_internal(sax);
if (result && !strict)
{
// the caller keeps using the input: position it right after the
// value by giving back the character that terminated it
m_lexer.restore_pending_unget();
}
// strict mode: next byte must be EOF
if (result && strict && (get_token() != token_type::end_of_input))
else if (result && strict && (get_token() != token_type::end_of_input))
{
return sax->parse_error(m_lexer.get_position(),
m_lexer.get_token_string(),