Owner sign-off: replace root LICENSE with Apache License 2.0, add a root NOTICE file, and swap the GPL-2.0 boilerplate header in every first-party core/ and obs-adapter/ source file for a short Apache-2.0 notice. This resolves review finding C2 (GPLv2 top-level LICENSE vs. the vendored Apache-2.0 LiveKit SDK is a license-compatibility violation): the whole repo is now Apache-2.0, matching LiveKit, so there's no GPL/Apache clash left. Updated the README Status gate and the CI workflow comment to reflect that C2 is resolved, while leaving the C1 WebRTC/OpenH264 patent/royalty gate untouched -- that question is still open and still blocks release. third_party/ stays under its own upstream licenses; only this project's own code changed hands. All 6 CTest suites still pass after the header swap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
436 lines
12 KiB
C++
436 lines
12 KiB
C++
/*
|
|
streamer-tools OBS Camera Plugin - minimal JSON reader
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
|
|
|
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 <cstdlib>
|
|
#include <cstring>
|
|
|
|
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<Value> a)
|
|
{
|
|
Value v;
|
|
v.type_ = Type::Array;
|
|
v.array_ = std::move(a);
|
|
return v;
|
|
}
|
|
|
|
Value Value::makeObject(std::map<std::string, Value> 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<std::string, Value> 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<Value> 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<std::size_t>(i)];
|
|
unsigned digit;
|
|
if (c >= '0' && c <= '9')
|
|
digit = static_cast<unsigned>(c - '0');
|
|
else if (c >= 'a' && c <= 'f')
|
|
digit = static_cast<unsigned>(c - 'a') + 10u;
|
|
else if (c >= 'A' && c <= 'F')
|
|
digit = static_cast<unsigned>(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<char>(cp));
|
|
} else if (cp < 0x800) {
|
|
out.push_back(static_cast<char>(0xC0u | (cp >> 6)));
|
|
out.push_back(static_cast<char>(0x80u | (cp & 0x3Fu)));
|
|
} else if (cp < 0x10000) {
|
|
out.push_back(static_cast<char>(0xE0u | (cp >> 12)));
|
|
out.push_back(static_cast<char>(0x80u | ((cp >> 6) & 0x3Fu)));
|
|
out.push_back(static_cast<char>(0x80u | (cp & 0x3Fu)));
|
|
} else {
|
|
out.push_back(static_cast<char>(0xF0u | (cp >> 18)));
|
|
out.push_back(static_cast<char>(0x80u | ((cp >> 12) & 0x3Fu)));
|
|
out.push_back(static_cast<char>(0x80u | ((cp >> 6) & 0x3Fu)));
|
|
out.push_back(static_cast<char>(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<unsigned char>(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<char>(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
|