我可以使用JsonCpp来部分验证JSON输入吗

Can I use JsonCpp to partially-validate JSON input?

本文关键字:验证 JSON 输入 可以使 JsonCpp 我可以      更新时间:2023-10-16

我使用JsonCpp在C++中解析JSON。

例如

Json::Reader r;
std::stringstream ss;
ss << "{"name": "sample"}";
Json::Value v;
assert(r.parse(ss, v));         // OK
assert(v["name"] == "sample");  // OK

但我的实际输入是JSON消息的整个,这些消息可能以任何大小的块到达;我所能做的就是让JsonCpp尝试逐个字符地解析我的输入,在发现它们时吃掉完整的JSON消息:

Json::Reader r;
std::string input = "{"name": "sample"}{"name": "aardvark"}";
for (size_t cursor = 0; cursor < input.size(); cursor++) {  
    std::stringstream ss;
    ss << input.substr(0, cursor);
    Json::Value v;
    if (r.parse(ss, v)) {
        std::cout << v["name"] << " ";
        input.erase(0, cursor);
    }
} // Output: sample aardvark

这已经有点令人讨厌了,但情况确实变得更糟了。当输入的一部分丢失时(无论出于何种原因),我还需要能够重新同步。

现在它不一定是无损的,但我想防止以下输入可能永远破坏解析器:

{"name": "samp{"name": "aardvark"}

将此输入传递给JsonCpp将失败,但当我们在缓冲区中接收到更多字符时,这个问题不会消失;则第二CCD_ 1直接在其之前的CCD_;缓冲区永远无法完成以呈现有效的JSON。

然而,如果我被告知片段在第二个n字符时肯定会变为无效,那么我可以将缓冲区中的所有内容都丢弃到该点,然后简单地等待下一个{来考虑新对象的开始,作为尽最大努力的重新同步。


那么,有没有一种方法可以让JsonCpp告诉我,JSON的一个不完整片段是否已经保证了完整的"对象"在语法上是无效的?

即:

{"name": "sample"}   Valid        (Json::Reader::parse == true)
{"name": "sam        Incomplete   (Json::Reader::parse == false)
{"name": "sam"LOL    Invalid      (Json::Reader::parse == false)

我想区分这两种失败状态。

我可以使用JsonCpp来实现这一点吗?还是我必须通过构建一个状态机来编写我自己的JSON"部分验证器",该状态机在输入字符串的每一步都考虑哪些字符是"有效的"?我宁愿不重新发明轮子。。。

这当然取决于你是否真的控制了数据包(以及生产者)。如果你这样做,最简单的方法是在标题中指示边界:

+---+---+---+---+-----------------------
| 3 | 16|132|243|endofprevious"}{"name":...
+---+---+---+---+-----------------------

标题很简单:

  • 3表示边界的数量
  • 16、132和243表示每个边界的位置,它们对应于新对象(或列表)的左括号

然后是缓冲器本身。

在接收到这样的数据包时,可以解析以下条目:

  • previous + current[0:16]
  • current[16:132]
  • current[132:243]

并且current[243:]被保存用于下一个数据包(尽管您总是可以尝试解析它,以防它完成)。

通过这种方式,数据包是自动同步的,并且没有模糊检测,以及它所带来的所有故障情况。

注意,在分组中可能存在0边界。它只是意味着一个对象足够大,可以跨越几个数据包,您只需要暂时积累。

我建议让数字表示"固定"(例如,每个4个字节),并确定字节顺序(您的机器的字节顺序),以便轻松地将它们转换为二进制。我认为开销相当小(假设name0已经是11个字节,则每个条目4个字节+4个字节)。

逐字符遍历缓冲区并手动检查:

  • 字母字符的存在
    • 字符串之外(不过要注意"可以用转义)
    • 不属于nulltruefalse
    • 不是eE,它看起来像是带指数的数字文字
  • 在字符串之外但紧接在"之后的数字的存在

并不是包罗万象,但我认为它涵盖了足够多的情况,可以在消息截断点或相当接近消息截断点时相当可靠地中断解析。

它正确地接受:

{"name": "samL
{"name": "sam0
{"name": "sam", 0
{"name": true

作为有效的JSON片段,但捕获:

{"name": "sam"L
{"name": "sam"0
{"name": "sam"true

被认为是不可接受的。

因此,以下输入都将导致完整的尾随对象被成功解析:

1. {"name": "samp{"name": "aardvark"}
   //            ^ ^
   //            A B    - B is point of failure.
   //                     Stripping leading `{` and scanning for the first
   //                      free `{` gets us to A. (*)
   {"name": "aardvark"}
2. {"name": "samp{"0": "abc"}
   //            ^ ^
   //            A B    - B is point of failure.
   //                     Stripping and scanning gets us to A.
   {"0": "abc"}
3. {"name":{ "samp{"0": "abc"}
   //      ^      ^ ^
   //      A      B C   - C is point of failure.
   //                     Stripping and scanning gets us to A.
   { "samp{"0": "abc"}
   //     ^ ^
   //     B C           - C is still point of failure.
   //                     Stripping and scanning gets us to B.
   {"0": "abc"}

我的实现通过了一些相当彻底的单元测试。尽管如此,我想知道这种方法本身是否可以在不增加复杂性的情况下得到改进。


*我实际上在每条消息前都加了一个sentinel字符串,这使得"剥离和扫描"部分更加可靠,而不是寻找一个前导"{"

只需看看expat或其他流式xml解析器。如果不是的话,jsoncpp的逻辑应该是相似的。(如果需要,请该库的开发人员改进流读取。)

换句话说,从我的角度来看:

  1. 如果你的一些网络(而不是JSON)数据包丢失了——这不是JSON解析器的问题,只需使用更可靠的协议或发明自己的协议。然后通过它传输JSON。

  2. 如果JSON解析器报告错误,并且此错误发生在最后一个解析的令牌上(流中没有更多数据,但预期会有),则累积数据并重试(此任务应由库本身完成)。

    但有时它可能不会报告错误。例如,当您转接123456时,只收到123。但这与您的情况不匹配,因为您不在单个JSON数据包中传输原始数据。

  3. 如果流包含有效数据包,然后是半接收数据包,则应为每个有效数据包调用一些回调。

  4. 如果JSON解析器报告错误,并且它确实是无效的JSON,则应关闭流,并在必要时再次打开。