catcrafts.net/tests/ShouldParseJson/main.cpp

116 lines
5.4 KiB
C++
Raw Normal View History

2026-08-15 00:54:05 +02:00
/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
*/
// The JSON reader. It parses provider payloads and fetched post lists —
// input from other people's servers — so the malformed-input half of this
// suite is the part that matters most: reject, never partially accept.
import std;
import Catcrafts.Shared;
using namespace Catcrafts;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what, std::string_view got = {}) {
if (ok) return;
++failures;
std::println(std::cerr, "FAIL: {}{}{}", what,
got.empty() ? "" : " got: ", got);
}
} // namespace
int main() {
using namespace Catcrafts::Json;
auto ok = [](std::string_view text) { return Parse(text).has_value(); };
auto bad = [](std::string_view text) { return !Parse(text).has_value(); };
// ── shapes ────────────────────────────────────────────────────────
Check(ok("{}"), "json: empty object");
Check(ok("[]"), "json: empty array");
Check(ok(" \n\t {\"a\": 1} \n "), "json: surrounding whitespace");
Check(ok("[1,2,3]"), "json: number array");
Check(ok("{\"a\":{\"b\":[true,false,null]}}"), "json: nesting");
// ── malformed input must be rejected, not partially accepted ──────
Check(bad("{"), "json: unterminated object");
Check(bad("[1,]"), "json: trailing comma");
Check(bad("{\"a\":1,}"), "json: trailing comma in object");
Check(bad("{'a':1}"), "json: single quotes");
Check(bad("\"unterminated"), "json: unterminated string");
Check(bad("{\"a\" 1}"), "json: missing colon");
Check(bad("nul"), "json: bad literal");
Check(bad("{} garbage"), "json: trailing content rejected");
Check(bad("[1,2] [3]"), "json: concatenated documents rejected");
Check(bad("\"raw\nnewline\""), "json: control char in string");
Check(bad("01"), "json: leading zero");
Check(bad("+1"), "json: leading plus");
Check(bad("1."), "json: trailing decimal point");
Check(bad(".5"), "json: bare fraction");
Check(bad("1e"), "json: empty exponent");
Check(bad("1e+"), "json: exponent sign with no digits");
Check(bad("-"), "json: lone minus");
Check(bad("1e400"), "json: out of double range");
Check(ok("0"), "json: zero");
Check(ok("-0"), "json: negative zero");
Check(ok("0.5"), "json: leading zero with fraction");
Check(ok("-1.5e-3"), "json: full number grammar");
Check(ok("1E+2"), "json: capital exponent");
Check(bad(""), "json: empty input");
// ── string decoding ───────────────────────────────────────────────
auto strOf = [](std::string_view doc) -> std::string {
auto v = Parse(doc);
if (!v || !v->IsObject()) return "<parse-failed>";
return std::string(v->Str("k"));
};
Check(strOf(R"({"k":"a\"b"})") == "a\"b", "json: escaped quote");
Check(strOf(R"({"k":"a\\b"})") == "a\\b", "json: escaped backslash");
Check(strOf(R"({"k":"a\nb"})") == "a\nb", "json: newline escape");
Check(strOf(R"({"k":"A"})") == "A", "json: \\u ascii");
Check(strOf(R"({"k":"é"})") == "é", "json: \\u latin-1");
Check(strOf(R"({"k":""})") == "", "json: \\u BMP");
// Astral plane arrives as a UTF-16 surrogate pair. Encoding each half
// separately yields invalid UTF-8 — emoji in Lemmy post titles are
// exactly this case, so it has to be combined.
Check(strOf(R"({"k":"😺"})") == "\U0001F63A", "json: surrogate pair -> emoji");
Check(strOf(R"({"k":"\ud83d"})") == "<EFBFBD>", "json: lone high surrogate -> U+FFFD");
Check(strOf(R"({"k":"\ude3a"})") == "<EFBFBD>", "json: lone low surrogate -> U+FFFD");
Check(strOf(R"({"k":"raw é "})") == "raw é ✓", "json: raw utf-8 passthrough");
// ── accessors ─────────────────────────────────────────────────────
auto doc = Parse(R"({"s":"x","n":42,"neg":-7,"b":true,"nul":null})");
Check(doc.has_value(), "json: accessor doc parses");
if (doc) {
Check(doc->Str("s") == "x", "json: Str");
Check(doc->Int("n") == 42, "json: Int");
Check(doc->Int("neg") == -7, "json: Int negative");
Check(doc->Bool("b"), "json: Bool");
Check(doc->Str("missing", "fallback") == "fallback", "json: Str fallback");
Check(doc->Int("missing", 99) == 99, "json: Int fallback");
// Wrong-typed field falls back rather than reinterpreting.
Check(doc->Int("s", 5) == 5, "json: type mismatch falls back");
Check(doc->Find("missing") == nullptr, "json: Find absent");
Check(doc->Find("nul") != nullptr && doc->Find("nul")->IsNull(),
"json: present-null distinguishable from absent");
}
// ── depth guard ───────────────────────────────────────────────────
std::string deep(200, '[');
Check(bad(deep), "json: deep nesting rejected, not stack overflow");
if (failures != 0) {
std::println(std::cerr, "{} check(s) failed", failures);
return 1;
}
return 0;
}