/* 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 ""; 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"})") == "�", "json: lone high surrogate -> U+FFFD"); Check(strOf(R"({"k":"\ude3a"})") == "�", "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; }