278 lines
7.8 KiB
C++
278 lines
7.8 KiB
C++
// A small, strict JSON parser. It only has to read the request bodies that the
|
|
// web UI sends, but it validates everything (depth limit, escapes, UTF-16
|
|
// surrogates) because those bodies arrive over a socket.
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace lab {
|
|
|
|
struct Json {
|
|
enum class Type { Null, Bool, Number, String, Array, Object };
|
|
|
|
Type type = Type::Null;
|
|
bool b = false;
|
|
double n = 0;
|
|
std::string s;
|
|
std::vector<Json> a;
|
|
std::vector<std::pair<std::string, Json>> o;
|
|
|
|
const Json* find(const std::string& key) const {
|
|
if (type != Type::Object) return nullptr;
|
|
for (const auto& kv : o) {
|
|
if (kv.first == key) return &kv.second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
std::string str(const std::string& key, const std::string& def = "") const {
|
|
const Json* v = find(key);
|
|
return (v && v->type == Type::String) ? v->s : def;
|
|
}
|
|
bool boolean(const std::string& key, bool def = false) const {
|
|
const Json* v = find(key);
|
|
return (v && v->type == Type::Bool) ? v->b : def;
|
|
}
|
|
double number(const std::string& key, double def = 0) const {
|
|
const Json* v = find(key);
|
|
return (v && v->type == Type::Number) ? v->n : def;
|
|
}
|
|
const std::vector<Json>* array(const std::string& key) const {
|
|
const Json* v = find(key);
|
|
return (v && v->type == Type::Array) ? &v->a : nullptr;
|
|
}
|
|
};
|
|
|
|
namespace detail {
|
|
|
|
class JsonParser {
|
|
public:
|
|
explicit JsonParser(const std::string& text) : t_(text) {}
|
|
|
|
bool parse(Json& out, std::string& error) {
|
|
skipWs();
|
|
if (!value(out, 0)) {
|
|
error = err_.empty() ? "invalid JSON" : err_;
|
|
return false;
|
|
}
|
|
skipWs();
|
|
if (pos_ != t_.size()) {
|
|
error = "unexpected text after the JSON value";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
const std::string& t_;
|
|
size_t pos_ = 0;
|
|
std::string err_;
|
|
|
|
bool fail(const char* msg) {
|
|
if (err_.empty()) err_ = std::string(msg) + " at offset " + std::to_string(pos_);
|
|
return false;
|
|
}
|
|
void skipWs() {
|
|
while (pos_ < t_.size() && (t_[pos_] == ' ' || t_[pos_] == '\t' || t_[pos_] == '\n' || t_[pos_] == '\r')) ++pos_;
|
|
}
|
|
bool literal(const char* word) {
|
|
size_t i = 0;
|
|
while (word[i]) {
|
|
if (pos_ + i >= t_.size() || t_[pos_ + i] != word[i]) return false;
|
|
++i;
|
|
}
|
|
pos_ += i;
|
|
return true;
|
|
}
|
|
|
|
static void appendUtf8(std::string& out, uint32_t cp) {
|
|
if (cp < 0x80) {
|
|
out += static_cast<char>(cp);
|
|
} else if (cp < 0x800) {
|
|
out += static_cast<char>(0xC0 | (cp >> 6));
|
|
out += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else if (cp < 0x10000) {
|
|
out += static_cast<char>(0xE0 | (cp >> 12));
|
|
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
out += static_cast<char>(0x80 | (cp & 0x3F));
|
|
} else {
|
|
out += static_cast<char>(0xF0 | (cp >> 18));
|
|
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
|
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
|
out += static_cast<char>(0x80 | (cp & 0x3F));
|
|
}
|
|
}
|
|
|
|
bool hex4(uint32_t& v) {
|
|
if (pos_ + 4 > t_.size()) return fail("truncated \\u escape");
|
|
v = 0;
|
|
for (int i = 0; i < 4; ++i) {
|
|
const char c = t_[pos_++];
|
|
v <<= 4;
|
|
if (c >= '0' && c <= '9') v |= static_cast<uint32_t>(c - '0');
|
|
else if (c >= 'a' && c <= 'f') v |= static_cast<uint32_t>(c - 'a' + 10);
|
|
else if (c >= 'A' && c <= 'F') v |= static_cast<uint32_t>(c - 'A' + 10);
|
|
else return fail("bad \\u escape");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool string(std::string& out) {
|
|
if (pos_ >= t_.size() || t_[pos_] != '"') return fail("expected a string");
|
|
++pos_;
|
|
out.clear();
|
|
while (pos_ < t_.size()) {
|
|
const unsigned char c = static_cast<unsigned char>(t_[pos_++]);
|
|
if (c == '"') return true;
|
|
if (c < 0x20) return fail("control character in string");
|
|
if (c != '\\') {
|
|
out += static_cast<char>(c);
|
|
continue;
|
|
}
|
|
if (pos_ >= t_.size()) break;
|
|
const char e = t_[pos_++];
|
|
switch (e) {
|
|
case '"': out += '"'; break;
|
|
case '\\': out += '\\'; break;
|
|
case '/': out += '/'; break;
|
|
case 'b': out += '\b'; break;
|
|
case 'f': out += '\f'; break;
|
|
case 'n': out += '\n'; break;
|
|
case 'r': out += '\r'; break;
|
|
case 't': out += '\t'; break;
|
|
case 'u': {
|
|
uint32_t cp = 0;
|
|
if (!hex4(cp)) return false;
|
|
if (cp >= 0xD800 && cp <= 0xDBFF) {
|
|
uint32_t lo = 0;
|
|
if (pos_ + 1 < t_.size() && t_[pos_] == '\\' && t_[pos_ + 1] == 'u') {
|
|
pos_ += 2;
|
|
if (!hex4(lo)) return false;
|
|
}
|
|
if (lo >= 0xDC00 && lo <= 0xDFFF) cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
|
|
else cp = 0xFFFD;
|
|
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
|
|
cp = 0xFFFD;
|
|
}
|
|
appendUtf8(out, cp);
|
|
break;
|
|
}
|
|
default: return fail("bad escape");
|
|
}
|
|
}
|
|
return fail("unterminated string");
|
|
}
|
|
|
|
bool number(double& out) {
|
|
const size_t start = pos_;
|
|
if (pos_ < t_.size() && t_[pos_] == '-') ++pos_;
|
|
bool digits = false;
|
|
while (pos_ < t_.size() && ((t_[pos_] >= '0' && t_[pos_] <= '9') || t_[pos_] == '.' || t_[pos_] == 'e' ||
|
|
t_[pos_] == 'E' || t_[pos_] == '+' || t_[pos_] == '-')) {
|
|
if (t_[pos_] >= '0' && t_[pos_] <= '9') digits = true;
|
|
++pos_;
|
|
}
|
|
if (!digits) return fail("bad number");
|
|
try {
|
|
out = std::stod(t_.substr(start, pos_ - start));
|
|
} catch (...) {
|
|
return fail("bad number");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool value(Json& out, int depth) {
|
|
if (depth > 32) return fail("nesting too deep");
|
|
if (pos_ >= t_.size()) return fail("unexpected end");
|
|
const char c = t_[pos_];
|
|
if (c == '{') {
|
|
++pos_;
|
|
out.type = Json::Type::Object;
|
|
skipWs();
|
|
if (pos_ < t_.size() && t_[pos_] == '}') {
|
|
++pos_;
|
|
return true;
|
|
}
|
|
for (;;) {
|
|
skipWs();
|
|
std::string key;
|
|
if (!string(key)) return false;
|
|
skipWs();
|
|
if (pos_ >= t_.size() || t_[pos_] != ':') return fail("expected ':'");
|
|
++pos_;
|
|
skipWs();
|
|
Json v;
|
|
if (!value(v, depth + 1)) return false;
|
|
out.o.emplace_back(std::move(key), std::move(v));
|
|
skipWs();
|
|
if (pos_ < t_.size() && t_[pos_] == ',') {
|
|
++pos_;
|
|
continue;
|
|
}
|
|
if (pos_ < t_.size() && t_[pos_] == '}') {
|
|
++pos_;
|
|
return true;
|
|
}
|
|
return fail("expected ',' or '}'");
|
|
}
|
|
}
|
|
if (c == '[') {
|
|
++pos_;
|
|
out.type = Json::Type::Array;
|
|
skipWs();
|
|
if (pos_ < t_.size() && t_[pos_] == ']') {
|
|
++pos_;
|
|
return true;
|
|
}
|
|
for (;;) {
|
|
skipWs();
|
|
Json v;
|
|
if (!value(v, depth + 1)) return false;
|
|
out.a.push_back(std::move(v));
|
|
skipWs();
|
|
if (pos_ < t_.size() && t_[pos_] == ',') {
|
|
++pos_;
|
|
continue;
|
|
}
|
|
if (pos_ < t_.size() && t_[pos_] == ']') {
|
|
++pos_;
|
|
return true;
|
|
}
|
|
return fail("expected ',' or ']'");
|
|
}
|
|
}
|
|
if (c == '"') {
|
|
out.type = Json::Type::String;
|
|
return string(out.s);
|
|
}
|
|
if (c == 't') {
|
|
out.type = Json::Type::Bool;
|
|
out.b = true;
|
|
return literal("true") || fail("bad literal");
|
|
}
|
|
if (c == 'f') {
|
|
out.type = Json::Type::Bool;
|
|
out.b = false;
|
|
return literal("false") || fail("bad literal");
|
|
}
|
|
if (c == 'n') {
|
|
out.type = Json::Type::Null;
|
|
return literal("null") || fail("bad literal");
|
|
}
|
|
out.type = Json::Type::Number;
|
|
return number(out.n);
|
|
}
|
|
};
|
|
|
|
} // namespace detail
|
|
|
|
inline bool parseJson(const std::string& text, Json& out, std::string& error) {
|
|
out = Json();
|
|
detail::JsonParser p(text);
|
|
return p.parse(out, error);
|
|
}
|
|
|
|
} // namespace lab
|