gnss-sim/3rdparty/boost/mysql/impl/format_sql.ipp

593 lines
18 KiB
C++

//
// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef BOOST_MYSQL_IMPL_FORMAT_SQL_IPP
#define BOOST_MYSQL_IMPL_FORMAT_SQL_IPP
#include <boost/mysql/blob_view.hpp>
#include <boost/mysql/character_set.hpp>
#include <boost/mysql/client_errc.hpp>
#include <boost/mysql/constant_string_view.hpp>
#include <boost/mysql/diagnostics.hpp>
#include <boost/mysql/error_code.hpp>
#include <boost/mysql/field_kind.hpp>
#include <boost/mysql/field_view.hpp>
#include <boost/mysql/format_sql.hpp>
#include <boost/mysql/string_view.hpp>
#include <boost/mysql/detail/access.hpp>
#include <boost/mysql/detail/escape_string.hpp>
#include <boost/mysql/detail/format_sql.hpp>
#include <boost/mysql/detail/output_string.hpp>
#include <boost/mysql/impl/internal/byte_to_hex.hpp>
#include <boost/mysql/impl/internal/call_next_char.hpp>
#include <boost/mysql/impl/internal/dt_to_string.hpp>
#include <boost/charconv/from_chars.hpp>
#include <boost/charconv/to_chars.hpp>
#include <boost/core/detail/string_view.hpp>
#include <boost/system/result.hpp>
#include <boost/system/system_error.hpp>
#include <boost/throw_exception.hpp>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
namespace boost {
namespace mysql {
namespace detail {
// Helpers to format fundamental types
inline void append_quoted_identifier(string_view name, format_context_base& ctx)
{
ctx.append_raw("`");
auto& impl = access::get_impl(ctx);
auto ec = detail::escape_string(name, impl.opts, '`', impl.output);
if (ec)
ctx.add_error(ec);
ctx.append_raw("`");
}
template <class T>
void append_int(T integer, format_context_base& ctx)
{
// Make sure our buffer is big enough. 2: sign + digits10 is only 1 below max
constexpr std::size_t buffsize = 32;
static_assert(2 + std::numeric_limits<double>::digits10 < buffsize, "");
char buff[buffsize];
auto res = charconv::to_chars(buff, buff + buffsize, integer);
// Can only fail becuase of buffer being too small
BOOST_ASSERT(res.ec == std::errc());
// Copy
access::get_impl(ctx).output.append(string_view(buff, res.ptr - buff));
}
inline void append_double(double number, format_context_base& ctx)
{
// Make sure our buffer is big enough. 4: sign, radix point, e+
// 3: max exponent digits
constexpr std::size_t buffsize = 32;
static_assert(4 + std::numeric_limits<double>::max_digits10 + 3 < buffsize, "");
// inf and nan are not supported by MySQL
if (std::isinf(number) || std::isnan(number))
{
ctx.add_error(client_errc::unformattable_value);
return;
}
char buff[buffsize];
// We format as scientific to make MySQL understand the number as a double.
// Otherwise, it takes it as a DECIMAL.
auto res = charconv::to_chars(buff, buff + buffsize, number, charconv::chars_format::scientific);
// Can only fail because of buffer being too small
BOOST_ASSERT(res.ec == std::errc());
// Copy
access::get_impl(ctx).output.append(string_view(buff, res.ptr - buff));
}
inline void append_quoted_string(string_view str, format_context_base& ctx)
{
auto& impl = access::get_impl(ctx);
impl.output.append("'");
auto ec = detail::escape_string(str, impl.opts, '\'', impl.output);
if (ec)
ctx.add_error(ec);
impl.output.append("'");
}
inline void append_string(string_view str, string_view format_spec, format_context_base& ctx)
{
// Parse format spec
if (format_spec.size() > 1u)
{
ctx.add_error(client_errc::format_string_invalid_specifier);
return;
}
// No specifier: quoted string
if (format_spec.empty())
return append_quoted_string(str, ctx);
// We got a specifier
switch (format_spec[0])
{
case 'i':
// format as identifier
return append_quoted_identifier(str, ctx);
case 'r':
// append raw SQL
ctx.append_raw(runtime(str));
break;
default: ctx.add_error(client_errc::format_string_invalid_specifier);
}
}
inline void append_blob(blob_view b, format_context_base& ctx)
{
// Blobs have a binary character set, which may include characters
// that are not valid in the current character set. However, escaping
// is always performed using the character_set_connection.
// mysql_real_escape_string escapes multibyte characters with a backslash,
// but this behavior is not documented, so we don't want to rely on it.
// The most reliable way to encode blobs is using hex strings.
// Output string
auto output = access::get_impl(ctx).output;
// We output characters to a temporary buffer, batching append calls
constexpr std::size_t buffer_size = 64;
char buffer[buffer_size]{};
char* it = buffer;
char* const end = buffer + buffer_size;
// Binary string introducer
output.append("x'");
// Serialize contents
for (unsigned char byte : b)
{
// Serialize the byte
it = byte_to_hex(byte, it);
// If we filled the buffer, dump it
if (it == end)
{
output.append({buffer, buffer_size});
it = buffer;
}
}
// Dump anything that didn't fill the buffer
output.append({buffer, static_cast<std::size_t>(it - buffer)});
// Closing quote
ctx.append_raw("'");
}
inline void append_quoted_date(date d, format_context_base& ctx)
{
char buffer[34];
buffer[0] = '\'';
std::size_t sz = detail::date_to_string(d.year(), d.month(), d.day(), span<char, 32>(buffer + 1, 32));
buffer[sz + 1] = '\'';
access::get_impl(ctx).output.append(string_view(buffer, sz + 2));
}
inline void append_quoted_datetime(datetime d, format_context_base& ctx)
{
char buffer[66];
buffer[0] = '\'';
std::size_t sz = detail::datetime_to_string(
d.year(),
d.month(),
d.day(),
d.hour(),
d.minute(),
d.second(),
d.microsecond(),
span<char, 64>(buffer + 1, 64)
);
buffer[sz + 1] = '\'';
access::get_impl(ctx).output.append(string_view(buffer, sz + 2));
}
inline void append_quoted_time(time t, format_context_base& ctx)
{
char buffer[66];
buffer[0] = '\'';
std::size_t sz = time_to_string(t, span<char, 64>(buffer + 1, 64));
buffer[sz + 1] = '\'';
access::get_impl(ctx).output.append(string_view(buffer, sz + 2));
}
inline void append_field_view(
field_view fv,
string_view format_spec,
bool allow_specs,
format_context_base& ctx
)
{
auto kind = fv.kind();
// String types may allow specs
if (allow_specs && kind == field_kind::string)
{
append_string(fv.get_string(), format_spec, ctx);
return;
}
// Reject specifiers if !allow_specs or for other types
if (!format_spec.empty())
{
ctx.add_error(client_errc::format_string_invalid_specifier);
return;
}
// Perform the formatting operation
switch (fv.kind())
{
case field_kind::null: ctx.append_raw("NULL"); return;
case field_kind::int64: return append_int(fv.get_int64(), ctx);
case field_kind::uint64: return append_int(fv.get_uint64(), ctx);
case field_kind::float_:
// float is formatted as double because it's parsed as such
return append_double(fv.get_float(), ctx);
case field_kind::double_: return append_double(fv.get_double(), ctx);
case field_kind::string: return append_quoted_string(fv.get_string(), ctx);
case field_kind::blob: return append_blob(fv.get_blob(), ctx);
case field_kind::date: return append_quoted_date(fv.get_date(), ctx);
case field_kind::datetime: return append_quoted_datetime(fv.get_datetime(), ctx);
case field_kind::time: return append_quoted_time(fv.get_time(), ctx);
default: BOOST_ASSERT(false); return;
}
}
// Helpers for parsing format strings
inline bool is_number(char c) { return c >= '0' && c <= '9'; }
inline bool is_name_start(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; }
inline bool is_format_spec_char(char c)
{
return c != '{' && c != '}' && static_cast<unsigned char>(c) >= 0x20 &&
static_cast<unsigned char>(c) <= 0x7e;
}
class format_state
{
format_context_base& ctx_;
span<const format_arg> args_;
// Borrowed from fmt
// 0: we haven't used any args yet
// -1: we're doing explicit indexing
// >0: we're doing auto indexing
int next_arg_id_{0};
BOOST_ATTRIBUTE_NODISCARD
bool advance(const char*& it, const char* end)
{
std::size_t size = detail::call_next_char(ctx_.impl_.opts.charset, it, end);
if (size == 0)
{
ctx_.add_error(client_errc::format_string_invalid_encoding);
return false;
}
it += size;
return true;
}
bool uses_auto_ids() const noexcept { return next_arg_id_ > 0; }
bool uses_explicit_ids() const noexcept { return next_arg_id_ == -1; }
void do_field(format_arg arg, string_view format_spec)
{
ctx_.format_arg(access::get_impl(arg).value, format_spec);
}
BOOST_ATTRIBUTE_NODISCARD
bool do_indexed_field(int arg_id, string_view format_spec)
{
BOOST_ASSERT(arg_id >= 0);
if (static_cast<std::size_t>(arg_id) >= args_.size())
{
ctx_.add_error(client_errc::format_arg_not_found);
return false;
}
do_field(args_[arg_id], format_spec);
return true;
}
struct arg_id_t
{
enum class type_t
{
none,
integral,
identifier
};
union data_t
{
unsigned short integral;
string_view identifier;
data_t() noexcept : integral{} {}
};
type_t type;
data_t data;
arg_id_t() noexcept : type(type_t::none), data() {}
arg_id_t(unsigned short v) noexcept : type(type_t::integral) { data.integral = v; }
arg_id_t(string_view v) noexcept : type(type_t::identifier) { data.identifier = v; }
};
BOOST_ATTRIBUTE_NODISCARD
static arg_id_t parse_arg_id(const char*& it, const char* format_end)
{
if (is_number(*it))
{
unsigned short field_index = 0;
auto res = charconv::from_chars(it, format_end, field_index);
if (res.ec != std::errc{})
return arg_id_t();
it = res.ptr;
return field_index;
}
else if (is_name_start(*it))
{
const char* name_begin = it;
while (it != format_end && (is_name_start(*it) || is_number(*it)))
++it;
string_view field_name(name_begin, it);
return field_name;
}
else
{
return arg_id_t();
}
}
BOOST_ATTRIBUTE_NODISCARD
static string_view parse_format_spec(const char*& it, const char* format_end)
{
if (it != format_end && *it == ':')
{
++it;
const char* first = it;
while (it != format_end && is_format_spec_char(*it))
++it;
return {first, it};
}
else
{
return string_view();
}
}
BOOST_ATTRIBUTE_NODISCARD
bool parse_field(const char*& it, const char* format_end)
{
// Taken from fmtlib and adapted to our requirements
// it points to the character next to the opening '{'
// replacement_field ::= "{" [arg_id] [":" (format_spec)] "}"
// arg_id ::= integer | identifier
// integer ::= <decimal, unsigned short, parsed by from_chars>
// identifier ::= id_start id_continue*
// id_start ::= "a"..."z" | "A"..."Z" | "_"
// id_continue ::= id_start | digit
// digit ::= "0"..."9"
// format_spec ::= <any character >= 0x20 && <= 0x7e && != "{", "}">
// Parse the ID and spec components
auto arg_id = parse_arg_id(it, format_end);
auto spec = parse_format_spec(it, format_end);
// If we're not at the end on the string, it's a syntax error
if (it == format_end || *it != '}')
{
ctx_.add_error(client_errc::format_string_invalid_syntax);
return false;
}
++it;
// Process what was parsed
switch (arg_id.type)
{
case arg_id_t::type_t::none: return append_auto_field(spec);
case arg_id_t::type_t::integral: return append_indexed_field(arg_id.data.integral, spec);
case arg_id_t::type_t::identifier: return append_named_field(arg_id.data.identifier, spec);
default: BOOST_ASSERT(false); return false;
}
}
BOOST_ATTRIBUTE_NODISCARD
bool append_named_field(string_view field_name, string_view format_spec)
{
// Find the argument
for (const auto& arg : args_)
{
if (access::get_impl(arg).name == field_name)
{
do_field(arg, format_spec);
return true;
}
}
// Not found
ctx_.add_error(client_errc::format_arg_not_found);
return false;
}
BOOST_ATTRIBUTE_NODISCARD
bool append_indexed_field(int index, string_view format_spec)
{
if (uses_auto_ids())
{
ctx_.add_error(client_errc::format_string_manual_auto_mix);
return false;
}
next_arg_id_ = -1;
return do_indexed_field(index, format_spec);
}
BOOST_ATTRIBUTE_NODISCARD
bool append_auto_field(string_view format_spec)
{
if (uses_explicit_ids())
{
ctx_.add_error(client_errc::format_string_manual_auto_mix);
return false;
}
return do_indexed_field(next_arg_id_++, format_spec);
}
public:
format_state(format_context_base& ctx, span<const format_arg> args) noexcept : ctx_(ctx), args_(args) {}
void format(string_view format_str)
{
// We can use operator++ when we know a character is ASCII. Some charsets
// allow ASCII continuation bytes, so we need to skip the entire character otherwise
auto cur_begin = format_str.data();
auto it = format_str.data();
auto end = format_str.data() + format_str.size();
while (it != end)
{
if (*it == '{')
{
// May be a replacement field or a literal brace. In any case, dump accumulated output
ctx_.impl_.output.append({cur_begin, it});
++it;
if (it == end)
{
// If the string ends here, it's en error
ctx_.add_error(client_errc::format_string_invalid_syntax);
return;
}
else if (*it == '{')
{
// A double brace is the escaped form of '{'
ctx_.append_raw("{");
++it;
}
else
{
// It's a replacement field. Process it
if (!parse_field(it, end))
return;
}
cur_begin = it;
}
else if (*it == '}')
{
// A lonely } is only legal as a escape curly brace (i.e. }})
ctx_.impl_.output.append({cur_begin, it});
++it;
if (it == end || *it != '}')
{
ctx_.add_error(client_errc::format_string_invalid_syntax);
return;
}
ctx_.impl_.output.append("}");
++it;
cur_begin = it;
}
else
{
if (!advance(it, end))
return;
}
}
// Dump any remaining SQL
ctx_.impl_.output.append({cur_begin, end});
}
};
} // namespace detail
} // namespace mysql
} // namespace boost
void boost::mysql::format_context_base::format_arg(detail::formattable_ref_impl arg, string_view format_spec)
{
switch (arg.type)
{
case detail::formattable_ref_impl::type_t::field:
detail::append_field_view(arg.data.fv, format_spec, false, *this);
break;
case detail::formattable_ref_impl::type_t::field_with_specs:
detail::append_field_view(arg.data.fv, format_spec, true, *this);
break;
case detail::formattable_ref_impl::type_t::fn_and_ptr:
if (!arg.data.custom.format_fn(arg.data.custom.obj, format_spec.begin(), format_spec.end(), *this))
{
add_error(client_errc::format_string_invalid_specifier);
}
break;
default: BOOST_ASSERT(false);
}
}
void boost::mysql::format_sql_to(
format_context_base& ctx,
constant_string_view format_str,
std::initializer_list<format_arg> args
)
{
detail::format_state(ctx, {args.begin(), args.end()}).format(format_str.get());
}
std::string boost::mysql::format_sql(
format_options opts,
constant_string_view format_str,
std::initializer_list<format_arg> args
)
{
format_context ctx(opts);
format_sql_to(ctx, format_str, args);
return std::move(ctx).get().value();
}
std::pair<bool, boost::mysql::string_view> boost::mysql::detail::parse_range_specifiers(
const char* spec_begin,
const char* spec_end
)
{
// range_format_spec ::= [":" [underlying_spec]]
// Example: {::i} => format an array of strings as identifiers
// Empty: no specifiers
if (spec_begin == spec_end)
return {true, {}};
// If the first character is not a ':', the spec is invalid.
if (*spec_begin != ':')
return {false, {}};
++spec_begin;
// Return the rest of the range
return {
true,
{spec_begin, spec_end}
};
}
#endif