// Glaze Library // For the license information refer to glaze.hpp #pragma once #include #include #include #include #include #include "glaze/core/opts.hpp" #include "glaze/json/write.hpp" #include "glaze/jsonb/header.hpp" #include "glaze/jsonb/text_decode.hpp" namespace glz { namespace jsonb_detail { // Emit a JSON string literal from a raw UTF-8 byte payload. Uses the JSON writer so all // control characters and structural JSON chars are correctly escaped. template inline void emit_raw_string_as_json(is_context auto& ctx, const char* data, size_t size, B& out, size_t& ix) { const sv s{data, size}; to::template op(s, ctx, out, ix); } // Emit a string whose bytes are already a valid JSON string literal body (i.e. with // RFC 8259 escapes already in the payload). We just wrap in quotes. template inline void emit_json_escaped_body(is_context auto& ctx, const char* data, size_t size, B& out, size_t& ix) { if (!ensure_space(ctx, out, ix + size + 2 + write_padding_bytes)) return; out[ix++] = static_cast::value_type>('"'); if (size) { std::memcpy(&out[ix], data, size); ix += size; } out[ix++] = static_cast::value_type>('"'); } template inline void jsonb_to_json_value(is_context auto& ctx, const uint8_t*& it, const uint8_t* end, B& out, size_t& ix, uint32_t depth); template inline void jsonb_to_json_container(is_context auto& ctx, const uint8_t* it, const uint8_t* stop, B& out, size_t& ix, char open, char close, uint32_t depth) { if (!ensure_space(ctx, out, ix + 2 + write_padding_bytes)) return; out[ix++] = static_cast::value_type>(open); bool first = true; while (it < stop) { if (!first) { if (!ensure_space(ctx, out, ix + 1 + write_padding_bytes)) return; out[ix++] = static_cast::value_type>(','); } first = false; jsonb_to_json_value(ctx, it, stop, out, ix, depth); if (bool(ctx.error)) return; // For objects, the next child is a key's matching value: emit ':' between key and value. if (close == '}') { if (it >= stop) { ctx.error = error_code::syntax_error; return; } if (!ensure_space(ctx, out, ix + 1 + write_padding_bytes)) return; out[ix++] = static_cast::value_type>(':'); jsonb_to_json_value(ctx, it, stop, out, ix, depth); if (bool(ctx.error)) return; } } if (it != stop) { ctx.error = error_code::syntax_error; return; } if (!ensure_space(ctx, out, ix + 1 + write_padding_bytes)) return; out[ix++] = static_cast::value_type>(close); } template inline void jsonb_to_json_value(is_context auto& ctx, const uint8_t*& it, const uint8_t* end, B& out, size_t& ix, uint32_t depth) { // DoS protection: cap recursion on pathologically nested blobs so untrusted input // can't blow the stack. Only containers bump depth below; scalar emission leaves // it alone. Matches the limit used by CBOR's converter. if (depth >= max_recursive_depth_limit) [[unlikely]] { ctx.error = error_code::exceeded_max_recursive_depth; return; } uint8_t tc{}; uint64_t sz{}; if (!jsonb::read_header(ctx, it, end, tc, sz)) return; if (static_cast(end - it) < sz) { ctx.error = error_code::unexpected_end; return; } const uint8_t* payload = it; it += sz; switch (tc) { // Spec: legacy implementations must interpret type 0/1/2 as null/true/false // even when the payload size is non-zero (forward-compatibility for future spec // extensions). Payload bytes are already skipped by `it += sz` above. case jsonb::type::null_: if (!ensure_space(ctx, out, ix + 4 + write_padding_bytes)) return; std::memcpy(&out[ix], "null", 4); ix += 4; return; case jsonb::type::true_: if (!ensure_space(ctx, out, ix + 4 + write_padding_bytes)) return; std::memcpy(&out[ix], "true", 4); ix += 4; return; case jsonb::type::false_: if (!ensure_space(ctx, out, ix + 5 + write_padding_bytes)) return; std::memcpy(&out[ix], "false", 5); ix += 5; return; case jsonb::type::int_: case jsonb::type::float_: { // Already valid JSON number text. if (!ensure_space(ctx, out, ix + sz + write_padding_bytes)) return; if (sz) std::memcpy(&out[ix], payload, sz); ix += sz; return; } case jsonb::type::int5: { // Parse and re-emit as decimal so the output is strict JSON. sv s{reinterpret_cast(payload), static_cast(sz)}; // Strip leading '+'. const char* p = s.data(); const char* e = p + s.size(); bool neg = false; if (p < e && *p == '+') ++p; if (p < e && *p == '-') { neg = true; ++p; } int64_t value = 0; if (e - p >= 2 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { uint64_t tmp = 0; auto [ptr, ec] = std::from_chars(p + 2, e, tmp, 16); if (ec != std::errc{} || ptr != e) { ctx.error = error_code::parse_number_failure; return; } value = neg ? -static_cast(tmp) : static_cast(tmp); } else { int64_t tmp = 0; auto [ptr, ec] = std::from_chars(p, e, tmp, 10); if (ec != std::errc{} || ptr != e) { ctx.error = error_code::parse_number_failure; return; } value = neg ? -tmp : tmp; } to::template op(value, ctx, out, ix); return; } case jsonb::type::float5: { sv s{reinterpret_cast(payload), static_cast(sz)}; if (s == "NaN") { // Strict JSON has no NaN representation; emit null (matches SQLite json()). if (!ensure_space(ctx, out, ix + 4 + write_padding_bytes)) return; std::memcpy(&out[ix], "null", 4); ix += 4; return; } if (s == "Infinity" || s == "+Infinity") { // Match SQLite json(): emit 9e999, which parses as +infinity in IEEE-754 binary64. if (!ensure_space(ctx, out, ix + 5 + write_padding_bytes)) return; std::memcpy(&out[ix], "9e999", 5); ix += 5; return; } if (s == "-Infinity") { if (!ensure_space(ctx, out, ix + 6 + write_padding_bytes)) return; std::memcpy(&out[ix], "-9e999", 6); ix += 6; return; } // Otherwise, best effort: strip leading '+'. const char* p = s.data(); const char* e = p + s.size(); if (p < e && *p == '+') ++p; const size_t n = static_cast(e - p); if (!ensure_space(ctx, out, ix + n + write_padding_bytes)) return; if (n) std::memcpy(&out[ix], p, n); ix += n; return; } case jsonb::type::text: // Spec: TEXT payload is already a valid JSON string body (no control chars, // no unescaped " or \), so wrap in quotes and memcpy — no scan needed. emit_json_escaped_body(ctx, reinterpret_cast(payload), static_cast(sz), out, ix); return; case jsonb::type::textraw: // Raw bytes that may require escaping — run through the JSON string writer. emit_raw_string_as_json(ctx, reinterpret_cast(payload), static_cast(sz), out, ix); return; case jsonb::type::textj: { // TEXTJ payload is already a valid JSON string body by spec. Emitting it // verbatim wrapped in quotes is correct. emit_json_escaped_body(ctx, reinterpret_cast(payload), static_cast(sz), out, ix); return; } case jsonb::type::text5: { // TEXT5 payloads may contain JSON5-only escapes (\xNN, \', \v, \0, line // continuations) that are not valid JSON. Decode to raw UTF-8 and re-emit via // the JSON string writer so the output is always strict JSON. std::string scratch; jsonb_detail::decode_text(ctx, jsonb::type::text5, payload, payload + sz, static_cast(sz), scratch); if (bool(ctx.error)) return; emit_raw_string_as_json(ctx, scratch.data(), scratch.size(), out, ix); return; } case jsonb::type::array: { const uint8_t* arr_stop = payload + sz; const uint8_t* ait = payload; jsonb_to_json_container(ctx, ait, arr_stop, out, ix, '[', ']', depth + 1); return; } case jsonb::type::object: { const uint8_t* obj_stop = payload + sz; const uint8_t* oit = payload; jsonb_to_json_container(ctx, oit, obj_stop, out, ix, '{', '}', depth + 1); return; } default: // Reserved types 13..15. ctx.error = error_code::syntax_error; return; } } } // Convert a JSONB blob to JSON text. template [[nodiscard]] inline error_ctx jsonb_to_json(const JSONBBuffer& input, JSONBuffer& out) { size_t ix{}; context ctx{}; const uint8_t* it = reinterpret_cast(input.data()); const uint8_t* end = it + input.size(); if (it >= end) { return {0, error_code::unexpected_end}; } jsonb_detail::jsonb_to_json_value(ctx, it, end, out, ix, 0); if (bool(ctx.error)) { return {0, ctx.error}; } if (it != end) { return {0, error_code::syntax_error}; } if constexpr (resizable) { out.resize(ix); } return {}; } template [[nodiscard]] inline expected jsonb_to_json(const JSONBBuffer& input) { std::string out; auto ec = jsonb_to_json(input, out); if (ec) { return unexpected(ec); } return out; } }