/* streamer-tools OBS Camera Plugin - JSON reader tests Copyright (C) 2026 CyberCoveLLC This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see */ #include #include #include "stplugin/json.h" #include "test_util.h" using stplugin::json::Value; using stplugin::json::parse; static void testRealResponses() { // The exact shape apps/server/src/obs/plugin.routes.ts returns. const Value slots = parse( R"({"slots":[{"identity":"cam1","displayName":"Alice","live":true},)" R"({"identity":"cam2","displayName":"Bob","live":false}]})"); ST_ASSERT(slots.valid()); ST_ASSERT(slots.isObject()); ST_ASSERT(slots["slots"].isArray()); ST_ASSERT_EQ(slots["slots"].size(), std::size_t(2)); ST_ASSERT_EQ(slots["slots"].at(0)["identity"].asString(), std::string("cam1")); ST_ASSERT_EQ(slots["slots"].at(0)["displayName"].asString(), std::string("Alice")); ST_ASSERT_EQ(slots["slots"].at(0)["live"].asBool(), true); ST_ASSERT_EQ(slots["slots"].at(1)["live"].asBool(true), false); const Value token = parse( R"({"lkToken":"eyJhbGciOiJIUzI1NiJ9.abc.def","wsUrl":"wss://streamers.example.com",)" R"("identity":"obs:main-room:Ab_1-cd2"})"); ST_ASSERT_EQ(token["lkToken"].asString(), std::string("eyJhbGciOiJIUzI1NiJ9.abc.def")); ST_ASSERT_EQ(token["wsUrl"].asString(), std::string("wss://streamers.example.com")); ST_ASSERT_EQ(token["identity"].asString(), std::string("obs:main-room:Ab_1-cd2")); const Value error = parse(R"({"error":"not found"})"); ST_ASSERT_EQ(error["error"].asString(), std::string("not found")); } // One backslash, as it appears in the JSON *text* being parsed. // // Every JSON input below that contains a backslash is built by concatenation // rather than written as a literal. Two separate portability problems make // the obvious spellings unsafe, both observed on the Windows CI runner: // - MSVC still forms escape sequences and universal-character-names inside // RAW string literals, which it must not: R"( ... backslash-u-d-8-3-d ... )" // is a hard compile error ("a universal-character-name specifies an // invalid character"), and a raw string containing backslash-slash is an // "illegal escape sequence". // - A doubled backslash immediately followed by 'u' inside an ordinary // literal sits on a genuinely murky corner of translation phase 1, where // compilers have historically disagreed about whether a // universal-character-name is formed. // Concatenation sidesteps both: no backslash is ever adjacent to a 'u' in // the source text at all. static const std::string kBS = "\\"; static void testScalarsAndEscapes() { ST_ASSERT(parse("null").isNull()); ST_ASSERT_EQ(parse("true").asBool(), true); ST_ASSERT_EQ(parse("false").asBool(true), false); ST_ASSERT_EQ(parse("0").asNumber(), 0.0); ST_ASSERT_EQ(parse("-12").asNumber(), -12.0); ST_ASSERT_EQ(parse("1.5e2").asNumber(), 150.0); ST_ASSERT_EQ(parse("\"\"").asString("x"), std::string("")); // "a\"b\\c\/d" -> a"b\c/d ST_ASSERT_EQ(parse("\"a" + kBS + "\"b" + kBS + kBS + "c" + kBS + "/d\"").asString(), std::string("a\"b\\c/d")); // "\n\t\r\b\f" ST_ASSERT_EQ(parse("\"" + kBS + "n" + kBS + "t" + kBS + "r" + kBS + "b" + kBS + "f\"").asString(), std::string("\n\t\r\b\f")); // \uXXXX escapes, including a surrogate pair (an emoji in a display name // is entirely plausible and must not corrupt the dropdown). ST_ASSERT_EQ(parse("\"" + kBS + "u0041\"").asString(), std::string("A")); ST_ASSERT_EQ(parse("\"caf" + kBS + "u00e9\"").asString(), std::string("caf\xc3\xa9")); ST_ASSERT_EQ(parse("\"" + kBS + "ud83d" + kBS + "ude00\"").asString(), std::string("\xf0\x9f\x98\x80")); // Whitespace everywhere legal. ST_ASSERT_EQ(parse(" {\n \"a\" :\t[ 1 , 2 ]\r\n} ")["a"].size(), std::size_t(2)); } static void testMalformedIsRejectedNotCrashed() { const std::vector bad = { "", " ", "{", "}", "[", "[1,", "[1,]", "{\"a\"}", "{\"a\":}", "{\"a\":1,}", "{a:1}", "{'a':1}", "\"unterminated", "\"bad" + kBS + "escape\"", // not a JSON escape character "\"" + kBS + "u00\"", // truncated code point "\"" + kBS + "uZZZZ\"", // non-hex code point "\"" + kBS + "ud83d\"", // lone high surrogate "\"" + kBS + "ude00\"", // lone low surrogate "01", // leading zero "+1", ".5", "1.", "1e", "1e+", "tru", "nulll", "{}garbage", // trailing content "[1,2] [3]", "\"raw\ncontrol\"", // literal control char inside a string "\xff\xfe", // binary garbage "502 Bad Gateway", }; for (const std::string &text : bad) { const Value v = parse(text); ST_ASSERT(!v.valid()); // Accessors on an invalid value must still be safe and return the // caller's fallback. ST_ASSERT_EQ(v["anything"].asString("fallback"), std::string("fallback")); ST_ASSERT_EQ(v.at(0).asNumber(-1.0), -1.0); ST_ASSERT_EQ(v.size(), std::size_t(0)); } } static void testDepthLimit() { // Deep-but-legal nesting is rejected rather than recursed into, so a // hostile response cannot overflow the stack inside OBS. std::string deep; const int depth = stplugin::json::kMaxDepth + 50; for (int i = 0; i < depth; ++i) deep += "["; for (int i = 0; i < depth; ++i) deep += "]"; ST_ASSERT(!parse(deep).valid()); // Just inside the limit still parses. std::string shallow; for (int i = 0; i < stplugin::json::kMaxDepth - 1; ++i) shallow += "["; shallow += "1"; for (int i = 0; i < stplugin::json::kMaxDepth - 1; ++i) shallow += "]"; ST_ASSERT(parse(shallow).valid()); } static void testWrongTypesFallBack() { const Value v = parse(R"({"n":5,"s":"x","b":true,"arr":[1],"obj":{}})"); ST_ASSERT_EQ(v["n"].asString("fallback"), std::string("fallback")); ST_ASSERT_EQ(v["s"].asNumber(-1.0), -1.0); ST_ASSERT_EQ(v["s"].asBool(true), true); ST_ASSERT_EQ(v["missing"].asString("fallback"), std::string("fallback")); ST_ASSERT_EQ(v["arr"].at(5).asNumber(-1.0), -1.0); ST_ASSERT_EQ(v["obj"].at(0).asNumber(-1.0), -1.0); ST_ASSERT_EQ(v["n"]["deeper"].asString("fallback"), std::string("fallback")); } int main() { testRealResponses(); testScalarsAndEscapes(); testMalformedIsRejectedNotCrashed(); testDepthLimit(); testWrongTypesFallBack(); return st_test_report("json"); }