All checks were successful
Deploy / build-deploy (push) Successful in 4m19s
116 lines
5.4 KiB
C++
116 lines
5.4 KiB
C++
/*
|
||
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;
|
||
}
|