Files
obs-streamer-tools-plugin/core/src/json.cpp
T
shadowdaoandClaude Sonnet 5 969b8db94a
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Successful in 12m10s
license: relicense first-party code from GPL-2.0-or-later to Apache-2.0
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
2026-09-07 04:44:16 -07:00

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