From 9839104635d4294e1f7b1cd16ffabcb4d68ef951 Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Fri, 19 Jun 2026 13:08:28 -0400 Subject: [PATCH] Make parser reject state sticky after WeaselJson_REJECT After the parser returns WeaselJson_REJECT once, subsequent calls to WeaselJsonParser_parse (including the final len==0 EOF call) must keep returning WeaselJson_REJECT instead of potentially reporting OK. - Add a `rejected` flag to Parser3. - Clear the flag in reset(). - Check the flag at the start of parse() and immediately return REJECT. - Set the flag whenever a continuation returns REJECT. Add a test covering the exact reproduction from issue #12. --- src/parser3.h | 17 +++++++++++++++-- src/test.cpp | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/parser3.h b/src/parser3.h index 04dcbd0..348f69b 100644 --- a/src/parser3.h +++ b/src/parser3.h @@ -139,6 +139,7 @@ struct Parser3 { stackPtr = stack(); std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF}); inKey = false; + rejected = false; utf8Codepoint = 0; utf16Surrogate = 0; minCodepoint = 0; @@ -161,6 +162,7 @@ struct Parser3 { NumDfa numDfa; Utf8Dfa strDfa; bool inKey = false; + bool rejected = false; #ifndef HAS_MUSTTAIL char *stashBufForTrampoline; @@ -1062,18 +1064,29 @@ constexpr inline struct ContinuationTable { inline WeaselJsonStatus Parser3::parse(char *buf, int len) { this->dataBegin = this->writeBuf = buf; + if (this->rejected) [[unlikely]] { + return WeaselJson_REJECT; + } + #ifdef HAS_MUSTTAIL // The continuation returns a value in 0..3 here (kBounce is only used by the // no-musttail trampoline below), so the conversion back to the enum is in // range. - return WeaselJsonStatus( - symbolTables.continuations[top()](this, buf, buf + len)); + ContinuationStatus status = + symbolTables.continuations[top()](this, buf, buf + len); + if (status == WeaselJson_REJECT) { + this->rejected = true; + } + return WeaselJsonStatus(status); #else this->stashBufForTrampoline = buf; ContinuationStatus result; while ((result = symbolTables.continuations[top()]( this, stashBufForTrampoline, buf + len)) == kBounce) ; + if (result == WeaselJson_REJECT) { + this->rejected = true; + } return WeaselJsonStatus(result); #endif } diff --git a/src/test.cpp b/src/test.cpp index f42f85d..4723341 100644 --- a/src/test.cpp +++ b/src/test.cpp @@ -202,6 +202,31 @@ TEST_CASE("parser3") { } } +TEST_CASE("rejected state is sticky") { + auto c = noopCallbacks(); + auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0); + REQUIRE(parser != nullptr); + + // Feed a valid prefix, then an invalid byte. The parser must reject. + std::string chunk1 = "1"; + REQUIRE(WeaselJsonParser_parse(parser, chunk1.data(), chunk1.size()) == + WeaselJson_AGAIN); + std::string chunk2 = "x"; + REQUIRE(WeaselJsonParser_parse(parser, chunk2.data(), chunk2.size()) == + WeaselJson_REJECT); + + // After a reject, every later call must also reject, including the EOF + // finish call that would otherwise return OK. + REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_REJECT); + + // Further data chunks must also stay rejected. + std::string chunk3 = " "; + REQUIRE(WeaselJsonParser_parse(parser, chunk3.data(), chunk3.size()) == + WeaselJson_REJECT); + + WeaselJsonParser_destroy(parser); +} + TEST_CASE("create rejects too-small stack") { auto c = noopCallbacks(); // The parser needs room for the bootstrap symbols pushed by reset(); a stack