transmission/libtransmission/variant-json.cc

694 lines
17 KiB
C++
Raw Normal View History

// This file Copyright © 2008-2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <array>
#include <cctype>
#include <cerrno> /* EILSEQ, EINVAL */
#include <cmath> /* fabs() */
#include <cstdlib>
#include <cstring>
#include <deque>
#include <string>
#include <string_view>
2022-07-26 02:45:54 +00:00
#include <utility>
#define UTF_CPP_CPLUSPLUS 201703L
#include <utf8.h>
#include <fmt/compile.h>
#include <fmt/format.h>
#include <jsonsl.h>
#define LIBTRANSMISSION_VARIANT_MODULE
#include "transmission.h"
#include "error.h"
#include "log.h"
#include "quark.h"
#include "tr-assert.h"
#include "tr-buffer.h"
#include "utils.h"
#include "variant-common.h"
#include "variant.h"
using namespace std::literals;
using Buffer = libtransmission::Buffer;
namespace
{
namespace parse_helpers
{
/* arbitrary value... this is much deeper than our code goes */
auto constexpr MaxDepth = size_t{ 64 };
struct json_wrapper_data
{
bool has_content;
size_t size;
std::string_view key;
std::string keybuf;
std::string strbuf;
tr_error* error;
std::deque<tr_variant*> stack;
tr_variant* top;
int parse_opts;
/* A very common pattern is for a container's children to be similar,
* e.g. they may all be objects with the same set of keys. So when
* a container is popped off the stack, remember its size to use as
* a preallocation heuristic for the next container at that depth. */
2022-01-17 18:39:50 +00:00
std::array<size_t, MaxDepth> preallocGuess;
};
tr_variant* get_node(struct jsonsl_st* jsn)
{
auto* data = static_cast<struct json_wrapper_data*>(jsn->data);
auto* parent = std::empty(data->stack) ? nullptr : data->stack.back();
tr_variant* node = nullptr;
if (parent == nullptr)
{
node = data->top;
}
else if (tr_variantIsList(parent))
{
node = tr_variantListAdd(parent);
}
else if (tr_variantIsDict(parent) && !std::empty(data->key))
{
node = tr_variantDictAdd(parent, tr_quark_new(data->key));
data->key = ""sv;
}
return node;
}
void error_handler(jsonsl_t jsn, jsonsl_error_t error, jsonsl_state_st* /*state*/, jsonsl_char_t const* buf)
{
auto* data = static_cast<struct json_wrapper_data*>(jsn->data);
tr_error_set(
&data->error,
EILSEQ,
fmt::format(
_("Couldn't parse JSON at position {position} '{text}': {error} ({error_code})"),
fmt::arg("position", jsn->pos),
fmt::arg("text", std::string_view{ buf, std::min(size_t{ 16U }, data->size - jsn->pos) }),
fmt::arg("error", jsonsl_strerror(error)),
fmt::arg("error_code", error)));
}
int error_callback(jsonsl_t jsn, jsonsl_error_t error, struct jsonsl_state_st* state, jsonsl_char_t* at)
{
error_handler(jsn, error, state, at);
return 0; /* bail */
}
void action_callback_PUSH(jsonsl_t jsn, jsonsl_action_t /*action*/, struct jsonsl_state_st* state, jsonsl_char_t const* /*buf*/)
{
auto* const data = static_cast<json_wrapper_data*>(jsn->data);
if ((state->type == JSONSL_T_LIST) || (state->type == JSONSL_T_OBJECT))
{
data->has_content = true;
tr_variant* node = get_node(jsn);
data->stack.push_back(node);
size_t const depth = std::size(data->stack);
2022-01-17 18:39:50 +00:00
size_t const n = depth < MaxDepth ? data->preallocGuess[depth] : 0;
if (state->type == JSONSL_T_LIST)
{
tr_variantInitList(node, n);
}
else
{
tr_variantInitDict(node, n);
}
}
}
2012-12-14 16:04:44 +00:00
/* like sscanf(in+2, "%4x", &val) but less slow */
[[nodiscard]] constexpr bool decode_hex_string(char const* in, unsigned int* setme)
{
TR_ASSERT(in != nullptr);
unsigned int val = 0;
char const* const end = in + 6;
TR_ASSERT(in[0] == '\\');
TR_ASSERT(in[1] == 'u');
in += 2;
do
{
val <<= 4;
if ('0' <= *in && *in <= '9')
{
val += *in - '0';
}
else if ('a' <= *in && *in <= 'f')
{
val += *in - 'a' + 10U;
}
else if ('A' <= *in && *in <= 'F')
{
val += *in - 'A' + 10U;
}
else
{
return false;
}
} while (++in != end);
*setme = val;
return true;
}
[[nodiscard]] std::string_view extract_escaped_string(char const* in, size_t in_len, std::string& buf)
{
char const* const in_end = in + in_len;
buf.clear();
while (in < in_end)
{
bool unescaped = false;
if (*in == '\\' && in_end - in >= 2)
{
switch (in[1])
{
case 'b':
buf.push_back('\b');
in += 2;
unescaped = true;
break;
case 'f':
buf.push_back('\f');
in += 2;
unescaped = true;
break;
case 'n':
buf.push_back('\n');
in += 2;
unescaped = true;
break;
case 'r':
buf.push_back('\r');
in += 2;
unescaped = true;
break;
case 't':
buf.push_back('\t');
in += 2;
unescaped = true;
break;
case '/':
buf.push_back('/');
in += 2;
unescaped = true;
break;
case '"':
buf.push_back('"');
in += 2;
unescaped = true;
break;
case '\\':
buf.push_back('\\');
in += 2;
unescaped = true;
break;
case 'u':
if (in_end - in >= 6)
{
unsigned int val = 0;
if (decode_hex_string(in, &val))
{
try
{
auto buf8 = std::array<char, 8>{};
auto const it = utf8::append(val, std::data(buf8));
buf.append(std::data(buf8), it - std::data(buf8));
}
catch (utf8::exception const&) // invalid codepoint
{
buf.push_back('?');
}
unescaped = true;
in += 6;
break;
}
}
}
}
if (!unescaped)
{
buf.push_back(*in);
++in;
}
}
return buf;
}
[[nodiscard]] std::pair<std::string_view, bool> extract_string(jsonsl_t jsn, struct jsonsl_state_st* state, std::string& buf)
{
// figure out where the string is
char const* in_begin = jsn->base + state->pos_begin;
if (*in_begin == '"')
{
in_begin++;
}
char const* const in_end = jsn->base + state->pos_cur;
size_t const in_len = in_end - in_begin;
if (memchr(in_begin, '\\', in_len) == nullptr)
{
/* it's not escaped */
return std::make_pair(std::string_view{ in_begin, in_len }, true);
}
return std::make_pair(extract_escaped_string(in_begin, in_len, buf), false);
}
void action_callback_POP(jsonsl_t jsn, jsonsl_action_t /*action*/, struct jsonsl_state_st* state, jsonsl_char_t const* /*buf*/)
{
auto* data = static_cast<struct json_wrapper_data*>(jsn->data);
if (state->type == JSONSL_T_STRING)
{
auto const [str, inplace] = extract_string(jsn, state, data->strbuf);
if (inplace && ((data->parse_opts & TR_VARIANT_PARSE_INPLACE) != 0))
{
tr_variantInitStrView(get_node(jsn), str);
}
else
{
tr_variantInitStr(get_node(jsn), str);
}
data->has_content = true;
}
else if (state->type == JSONSL_T_HKEY)
{
data->has_content = true;
auto const [key, inplace] = extract_string(jsn, state, data->keybuf);
data->key = key;
}
else if (state->type == JSONSL_T_LIST || state->type == JSONSL_T_OBJECT)
{
auto const depth = std::size(data->stack);
auto const* const v = data->stack.back();
data->stack.pop_back();
2022-01-17 18:39:50 +00:00
if (depth < MaxDepth)
{
data->preallocGuess[depth] = v->val.l.count;
}
}
else if (state->type == JSONSL_T_SPECIAL)
{
if ((state->special_flags & JSONSL_SPECIALf_NUMNOINT) != 0)
{
auto sv = std::string_view{ jsn->base + state->pos_begin, jsn->pos - state->pos_begin };
tr_variantInitReal(get_node(jsn), tr_parseNum<double>(sv).value_or(0.0));
}
else if ((state->special_flags & JSONSL_SPECIALf_NUMERIC) != 0)
{
char const* begin = jsn->base + state->pos_begin;
data->has_content = true;
tr_variantInitInt(get_node(jsn), std::strtoll(begin, nullptr, 10));
}
else if ((state->special_flags & JSONSL_SPECIALf_BOOLEAN) != 0)
{
bool const b = (state->special_flags & JSONSL_SPECIALf_TRUE) != 0;
data->has_content = true;
tr_variantInitBool(get_node(jsn), b);
}
else if ((state->special_flags & JSONSL_SPECIALf_NULL) != 0)
{
data->has_content = true;
tr_variantInitQuark(get_node(jsn), TR_KEY_NONE);
}
}
}
} // namespace parse_helpers
} // namespace
bool tr_variantParseJson(tr_variant& setme, int parse_opts, std::string_view json, char const** setme_end, tr_error** error)
{
using namespace parse_helpers;
TR_ASSERT((parse_opts & TR_VARIANT_PARSE_JSON) != 0);
auto data = json_wrapper_data{};
2022-01-17 18:39:50 +00:00
jsonsl_t jsn = jsonsl_new(MaxDepth);
jsn->action_callback_PUSH = action_callback_PUSH;
jsn->action_callback_POP = action_callback_POP;
jsn->error_callback = error_callback;
jsn->data = &data;
jsonsl_enable_all_callbacks(jsn);
data.error = nullptr;
data.size = std::size(json);
data.has_content = false;
data.key = ""sv;
data.parse_opts = parse_opts;
data.preallocGuess = {};
data.stack = {};
data.top = &setme;
/* parse it */
jsonsl_feed(jsn, static_cast<jsonsl_char_t const*>(std::data(json)), std::size(json));
/* EINVAL if there was no content */
if (data.error == nullptr && !data.has_content)
{
tr_error_set(&data.error, EINVAL, "No content");
}
/* maybe set the end ptr */
if (setme_end != nullptr)
{
*setme_end = std::data(json) + jsn->pos;
}
/* cleanup */
auto const success = data.error == nullptr;
if (data.error != nullptr)
{
tr_error_propagate(error, &data.error);
}
jsonsl_destroy(jsn);
return success;
}
// ---
namespace
{
namespace to_string_helpers
{
struct ParentState
{
int variantType;
int childIndex;
int childCount;
};
struct JsonWalk
{
explicit JsonWalk(bool do_indent)
: doIndent{ do_indent }
{
}
std::deque<ParentState> parents;
Buffer out;
bool doIndent;
};
void jsonIndent(struct JsonWalk* data)
{
static auto buf = std::array<char, 1024>{};
if (buf.front() == '\0')
{
memset(std::data(buf), ' ', std::size(buf));
buf[0] = '\n';
}
if (data->doIndent)
{
data->out.add(std::data(buf), std::size(data->parents) * 4 + 1);
}
}
void jsonChildFunc(struct JsonWalk* data)
{
if (!std::empty(data->parents))
{
auto& pstate = data->parents.back();
switch (pstate.variantType)
{
case TR_VARIANT_TYPE_DICT:
{
int const i = pstate.childIndex;
++pstate.childIndex;
if (i % 2 == 0)
{
data->out.add(data->doIndent ? ": "sv : ":"sv);
}
else
{
bool const is_last = pstate.childIndex == pstate.childCount;
if (!is_last)
{
data->out.push_back(',');
jsonIndent(data);
}
}
break;
}
case TR_VARIANT_TYPE_LIST:
++pstate.childIndex;
if (bool const is_last = pstate.childIndex == pstate.childCount; !is_last)
{
data->out.push_back(',');
jsonIndent(data);
}
break;
default:
break;
}
}
}
void jsonPushParent(struct JsonWalk* data, tr_variant const* v)
{
int const n_children = tr_variantIsDict(v) ? v->val.l.count * 2 : v->val.l.count;
data->parents.push_back({ v->type, 0, n_children });
}
void jsonPopParent(struct JsonWalk* data)
{
data->parents.pop_back();
}
void jsonIntFunc(tr_variant const* val, void* vdata)
{
auto buf = std::array<char, 64>{};
auto const* const out = fmt::format_to(std::data(buf), FMT_COMPILE("{:d}"), val->val.i);
auto* const data = static_cast<JsonWalk*>(vdata);
data->out.add(std::data(buf), static_cast<size_t>(out - std::data(buf)));
jsonChildFunc(data);
}
void jsonBoolFunc(tr_variant const* val, void* vdata)
{
auto* data = static_cast<struct JsonWalk*>(vdata);
if (val->val.b)
{
data->out.add("true"sv);
}
else
{
data->out.add("false"sv);
}
jsonChildFunc(data);
}
void jsonRealFunc(tr_variant const* val, void* vdata)
{
auto* data = static_cast<struct JsonWalk*>(vdata);
if (fabs(val->val.d - (int)val->val.d) < 0.00001)
{
auto buf = std::array<char, 64>{};
auto const* const out = fmt::format_to(std::data(buf), FMT_COMPILE("{:.0f}"), val->val.d);
data->out.add(std::data(buf), static_cast<size_t>(out - std::data(buf)));
}
else
{
auto buf = std::array<char, 64>{};
auto const* const out = fmt::format_to(std::data(buf), FMT_COMPILE("{:.4f}"), val->val.d);
data->out.add(std::data(buf), static_cast<size_t>(out - std::data(buf)));
}
jsonChildFunc(data);
}
void jsonStringFunc(tr_variant const* val, void* vdata)
{
auto* data = static_cast<struct JsonWalk*>(vdata);
auto sv = std::string_view{};
(void)!tr_variantGetStrView(val, &sv);
auto& out = data->out;
out.reserve(std::size(data->out) + std::size(sv) * 6 + 2);
out.push_back('"');
for (; !std::empty(sv); sv.remove_prefix(1))
{
switch (sv.front())
{
case '\b':
out.add(R"(\b)"sv);
break;
case '\f':
out.add(R"(\f)"sv);
break;
case '\n':
out.add(R"(\n)"sv);
break;
case '\r':
out.add(R"(\r)"sv);
break;
case '\t':
out.add(R"(\t)"sv);
break;
case '"':
out.add(R"(\")"sv);
break;
case '\\':
out.add(R"(\\)"sv);
break;
default:
if (isprint((unsigned char)sv.front()) != 0)
{
out.push_back(sv.front());
}
else
{
try
{
auto arr = std::array<char, 16>{};
auto const* const begin8 = std::data(sv);
auto const* const end8 = begin8 + std::size(sv);
auto const* walk8 = begin8;
auto const uch32 = utf8::next(walk8, end8);
auto const result = fmt::format_to_n(std::data(arr), std::size(arr), FMT_COMPILE("\\u{:04x}"), uch32);
out.add(std::data(arr), result.size);
sv.remove_prefix(walk8 - begin8 - 1);
}
fix: sonarcloud warnings (#2260) * fix: add-explicit-keyword-to-constructor warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuB-2farvsyMPiZl&open=AX1tyuB-2farvsyMPiZl * fix: use a structured binding declaration Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuHH2farvsyMPiZm&open=AX1tyuHH2farvsyMPiZm * Fix implicit-conversion-loses-precision warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuH22farvsyMPiZn&open=AX1tyuH22farvsyMPiZn * fix use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuO42farvsyMPiZw&open=AX1tyuO42farvsyMPiZw * fix implicit-conversion-loses-precision warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuO42farvsyMPiZs&open=AX1tyuO42farvsyMPiZs * fix: implicit-conversion-loses-precision warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1tyuO42farvsyMPiZt&open=AX1tyuO42farvsyMPiZt * fix handle-this-exceeption-or-do-not-catch-it warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1y7t12cIz3krp3mVpV&open=AX1y7t12cIz3krp3mVpV * fix make-this-variable-a-reference-to-const warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1y7t12cIz3krp3mVpW&open=AX1y7t12cIz3krp3mVpW * fix replace-redundant-type-with-auto warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1y7t12cIz3krp3mVpY&open=AX1y7t12cIz3krp3mVpY * Fix use-try_emplace-instead-of-emplace warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0WPk3QwxvzFbF6whWN&open=AX0WPk3QwxvzFbF6whWN * Fix: make-this-a-pointer-to-const warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0bZPUXjI_aUyxGBxB4&open=AX0bZPUXjI_aUyxGBxB4 * Fix use-std::make_unique warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0WPk0twxvzFbF6whWC&open=AX0WPk0twxvzFbF6whWC * Fix: replace-push_back-with-emplace_back warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0BraL_0QLXO0pQ_Bfv&open=AX0BraL_0QLXO0pQ_Bfv * Fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0bZPXajI_aUyxGBxB8&open=AX0bZPXajI_aUyxGBxB8 * Fix: replace-redundant-type-with-auto warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0bZPXajI_aUyxGBxB9&open=AX0bZPXajI_aUyxGBxB9 * Fix: replace-emplace-with-try_emplace warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0bZPXajI_aUyxGBxB_&open=AX0bZPXajI_aUyxGBxB_ * Fix: extract-assignment-from-operation warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0bZPXajI_aUyxGBxCA&open=AX0bZPXajI_aUyxGBxCA * Fix use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX1op6_hUBCnp-Grmra8&open=AX1op6_hUBCnp-Grmra8 * fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0L8qgMUnpKTxYiu5CN&open=AX0L8qgMUnpKTxYiu5CN * Revert "Fix use-std::make_unique warning" This reverts commit 463a742f344e495416621825a7203d168f26343f. * fix: make-parameter-a-pointer-to-const warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX06St241usi2gyYkPTZ&open=AX06St241usi2gyYkPTZ * fix: make-parameter-a-pointer-to-const warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX06St241usi2gyYkPTa&open=AX06St241usi2gyYkPTa * fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX01Itn2f_SST5i7BN1o&open=AX01Itn2f_SST5i7BN1o * fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AXziv55-MuQoG-Y-NgLX&open=AXziv55-MuQoG-Y-NgLX * fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AX0_a_JGNJn7rAzml_68&open=AX0_a_JGNJn7rAzml_68 * fix: replace-redundant-type-with-auto warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AXzyMRYbK9dvryvWm8XA&open=AXzyMRYbK9dvryvWm8XA * fix: use-init-statement warning Xref: https://sonarcloud.io/project/issues?id=transmission_transmission&issues=AXziv52fMuQoG-Y-NgLW&open=AXziv52fMuQoG-Y-NgLW
2021-12-01 23:11:57 +00:00
catch (utf8::exception const&)
{
out.push_back('?');
}
}
break;
}
}
out.push_back('"');
jsonChildFunc(data);
}
void jsonDictBeginFunc(tr_variant const* val, void* vdata)
{
auto* data = static_cast<struct JsonWalk*>(vdata);
jsonPushParent(data, val);
data->out.push_back('{');
if (val->val.l.count != 0)
{
jsonIndent(data);
}
}
void jsonListBeginFunc(tr_variant const* val, void* vdata)
{
size_t const n_children = tr_variantListSize(val);
auto* data = static_cast<struct JsonWalk*>(vdata);
jsonPushParent(data, val);
data->out.push_back('[');
if (n_children != 0)
{
jsonIndent(data);
}
}
void jsonContainerEndFunc(tr_variant const* val, void* vdata)
{
auto* data = static_cast<struct JsonWalk*>(vdata);
jsonPopParent(data);
jsonIndent(data);
if (tr_variantIsDict(val))
{
data->out.push_back('}');
}
else /* list */
{
data->out.push_back(']');
}
jsonChildFunc(data);
}
struct VariantWalkFuncs const walk_funcs = {
jsonIntFunc, //
jsonBoolFunc, //
jsonRealFunc, //
jsonStringFunc, //
jsonDictBeginFunc, //
jsonListBeginFunc, //
jsonContainerEndFunc, //
};
} // namespace to_string_helpers
} // namespace
std::string tr_variantToStrJson(tr_variant const* top, bool lean)
{
using namespace to_string_helpers;
auto data = JsonWalk{ !lean };
tr_variantWalk(top, &walk_funcs, &data, true);
auto& buf = data.out;
if (!std::empty(buf))
{
buf.push_back('\n');
}
2023-01-28 02:12:09 +00:00
return buf.to_string();
}