/* streamer-tools OBS Camera Plugin - minimal JSON reader Copyright (C) 2026 CyberCoveLLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ #include "stplugin/json.h" #include #include namespace stplugin { namespace json { namespace { const Value &invalidSingleton() { static const Value v; return v; } } // namespace Value Value::makeNull() { Value v; v.type_ = Type::Null; return v; } Value Value::makeBool(bool b) { Value v; v.type_ = Type::Bool; v.bool_ = b; return v; } Value Value::makeNumber(double n) { Value v; v.type_ = Type::Number; v.number_ = n; return v; } Value Value::makeString(std::string s) { Value v; v.type_ = Type::String; v.string_ = std::move(s); return v; } Value Value::makeArray(std::vector a) { Value v; v.type_ = Type::Array; v.array_ = std::move(a); return v; } Value Value::makeObject(std::map o) { Value v; v.type_ = Type::Object; v.object_ = std::move(o); return v; } const Value &Value::operator[](const std::string &key) const { if (type_ != Type::Object) return invalidSingleton(); auto it = object_.find(key); if (it == object_.end()) return invalidSingleton(); return it->second; } const Value &Value::at(std::size_t index) const { if (type_ != Type::Array || index >= array_.size()) return invalidSingleton(); return array_[index]; } std::size_t Value::size() const { if (type_ == Type::Array) return array_.size(); if (type_ == Type::Object) return object_.size(); if (type_ == Type::String) return string_.size(); return 0; } std::string Value::asString(const std::string &fallback) const { return type_ == Type::String ? string_ : fallback; } bool Value::asBool(bool fallback) const { return type_ == Type::Bool ? bool_ : fallback; } double Value::asNumber(double fallback) const { return type_ == Type::Number ? number_ : fallback; } // --------------------------------------------------------------------------- // Parser // --------------------------------------------------------------------------- namespace { class Parser { public: explicit Parser(const std::string &text) : s_(text) {} bool parseDocument(Value &out) { skipWs(); if (!parseValue(out, 0)) return false; skipWs(); // Trailing content is an error: "{}garbage" must not silently parse // as an empty object. return pos_ == s_.size(); } private: const std::string &s_; std::size_t pos_ = 0; bool eof() const { return pos_ >= s_.size(); } char peek() const { return s_[pos_]; } void skipWs() { while (!eof()) { const char c = s_[pos_]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; else break; } } bool literal(const char *lit) { const std::size_t n = std::strlen(lit); if (s_.compare(pos_, n, lit) != 0) return false; pos_ += n; return true; } bool parseValue(Value &out, int depth) { if (depth > kMaxDepth) return false; if (eof()) return false; switch (peek()) { case '{': return parseObject(out, depth); case '[': return parseArray(out, depth); case '"': { std::string str; if (!parseString(str)) return false; out = Value::makeString(std::move(str)); return true; } case 't': if (!literal("true")) return false; out = Value::makeBool(true); return true; case 'f': if (!literal("false")) return false; out = Value::makeBool(false); return true; case 'n': if (!literal("null")) return false; out = Value::makeNull(); return true; default: return parseNumber(out); } } bool parseObject(Value &out, int depth) { ++pos_; // '{' std::map members; skipWs(); if (!eof() && peek() == '}') { ++pos_; out = Value::makeObject(std::move(members)); return true; } for (;;) { skipWs(); std::string key; if (!parseString(key)) return false; skipWs(); if (eof() || peek() != ':') return false; ++pos_; skipWs(); Value v; if (!parseValue(v, depth + 1)) return false; members[key] = std::move(v); skipWs(); if (eof()) return false; if (peek() == ',') { ++pos_; continue; } if (peek() == '}') { ++pos_; out = Value::makeObject(std::move(members)); return true; } return false; } } bool parseArray(Value &out, int depth) { ++pos_; // '[' std::vector items; skipWs(); if (!eof() && peek() == ']') { ++pos_; out = Value::makeArray(std::move(items)); return true; } for (;;) { skipWs(); Value v; if (!parseValue(v, depth + 1)) return false; items.push_back(std::move(v)); skipWs(); if (eof()) return false; if (peek() == ',') { ++pos_; continue; } if (peek() == ']') { ++pos_; out = Value::makeArray(std::move(items)); return true; } return false; } } bool parseHex4(unsigned &out) { if (pos_ + 4 > s_.size()) return false; unsigned value = 0; for (int i = 0; i < 4; ++i) { const char c = s_[pos_ + static_cast(i)]; unsigned digit; if (c >= '0' && c <= '9') digit = static_cast(c - '0'); else if (c >= 'a' && c <= 'f') digit = static_cast(c - 'a') + 10u; else if (c >= 'A' && c <= 'F') digit = static_cast(c - 'A') + 10u; else return false; value = (value << 4) | digit; } pos_ += 4; out = value; return true; } static void appendUtf8(std::string &out, unsigned cp) { if (cp < 0x80) { out.push_back(static_cast(cp)); } else if (cp < 0x800) { out.push_back(static_cast(0xC0u | (cp >> 6))); out.push_back(static_cast(0x80u | (cp & 0x3Fu))); } else if (cp < 0x10000) { out.push_back(static_cast(0xE0u | (cp >> 12))); out.push_back(static_cast(0x80u | ((cp >> 6) & 0x3Fu))); out.push_back(static_cast(0x80u | (cp & 0x3Fu))); } else { out.push_back(static_cast(0xF0u | (cp >> 18))); out.push_back(static_cast(0x80u | ((cp >> 12) & 0x3Fu))); out.push_back(static_cast(0x80u | ((cp >> 6) & 0x3Fu))); out.push_back(static_cast(0x80u | (cp & 0x3Fu))); } } bool parseString(std::string &out) { if (eof() || peek() != '"') return false; ++pos_; out.clear(); for (;;) { if (eof()) return false; // unterminated string const unsigned char c = static_cast(s_[pos_]); if (c == '"') { ++pos_; return true; } if (c == '\\') { ++pos_; if (eof()) return false; const char esc = s_[pos_++]; switch (esc) { case '"': out.push_back('"'); break; case '\\': out.push_back('\\'); break; case '/': out.push_back('/'); break; case 'b': out.push_back('\b'); break; case 'f': out.push_back('\f'); break; case 'n': out.push_back('\n'); break; case 'r': out.push_back('\r'); break; case 't': out.push_back('\t'); break; case 'u': { unsigned cp = 0; if (!parseHex4(cp)) return false; if (cp >= 0xD800 && cp <= 0xDBFF) { // High surrogate: a low surrogate must follow. if (pos_ + 1 < s_.size() && s_[pos_] == '\\' && s_[pos_ + 1] == 'u') { pos_ += 2; unsigned lo = 0; if (!parseHex4(lo)) return false; if (lo < 0xDC00 || lo > 0xDFFF) return false; cp = 0x10000u + ((cp - 0xD800u) << 10) + (lo - 0xDC00u); } else { return false; } } else if (cp >= 0xDC00 && cp <= 0xDFFF) { return false; // lone low surrogate } appendUtf8(out, cp); break; } default: return false; } continue; } if (c < 0x20) return false; // raw control character out.push_back(static_cast(c)); ++pos_; } } bool parseNumber(Value &out) { const std::size_t start = pos_; if (!eof() && peek() == '-') ++pos_; if (eof()) return false; if (peek() == '0') { ++pos_; } else if (peek() >= '1' && peek() <= '9') { while (!eof() && peek() >= '0' && peek() <= '9') ++pos_; } else { return false; } if (!eof() && peek() == '.') { ++pos_; if (eof() || peek() < '0' || peek() > '9') return false; while (!eof() && peek() >= '0' && peek() <= '9') ++pos_; } if (!eof() && (peek() == 'e' || peek() == 'E')) { ++pos_; if (!eof() && (peek() == '+' || peek() == '-')) ++pos_; if (eof() || peek() < '0' || peek() > '9') return false; while (!eof() && peek() >= '0' && peek() <= '9') ++pos_; } const std::string token = s_.substr(start, pos_ - start); // strtod is locale-sensitive for the decimal separator, but the // grammar above only ever hands it ASCII digits with a '.', and OBS // does not switch the C locale away from "C". Using strtod rather // than std::stod keeps this noexcept. out = Value::makeNumber(std::strtod(token.c_str(), nullptr)); return true; } }; } // namespace Value parse(const std::string &text) { Parser p(text); Value v; if (!p.parseDocument(v)) return Value::invalid(); return v; } } // namespace json } // namespace stplugin