// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2018 Jason Volk // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice is present in all copies. The // full license for this software is available in the LICENSE file. namespace ircd::fmt { using namespace ircd::spirit; struct spec; struct specifier; struct bool_specifier extern const bool_specifier; struct char_specifier extern const char_specifier; struct signed_specifier extern const signed_specifier; struct unsigned_specifier extern const unsigned_specifier; struct float_specifier extern const float_specifier; struct hex_uppercase_specifier extern const hex_uppercase_specifier; struct hex_lowercase_specifier extern const hex_lowercase_specifier; struct pointer_specifier extern const pointer_specifier; struct string_specifier extern const string_specifier; constexpr char SPECIFIER {'%'}; constexpr char SPECIFIER_TERMINATOR {'$'}; template static bool generate_string(char *&out, const size_t &max, generator&&, const arg &val); template static bool generate(mutable_buffer &, gen&&, attr&&...); template static bool visit_type(const arg &val, lambda&& closure); static void handle_specifier(mutable_buffer &out, const uint &idx, const spec &, const arg &); } /// Structural representation of a format specifier. The parse of each /// specifier in the format string creates one of these. struct [[gnu::visibility("internal")]] ircd::fmt::spec { char sign {'+'}; char pad {' '}; ushort width {0}; ushort precision {0}; string_view name; }; /// Reflects the fmt::spec struct to allow the spirit::qi grammar to directly /// fill in the spec struct. #pragma GCC visibility push(internal) BOOST_FUSION_ADAPT_STRUCT ( ircd::fmt::spec, ( decltype(ircd::fmt::spec::sign), sign ) ( decltype(ircd::fmt::spec::pad), pad ) ( decltype(ircd::fmt::spec::width), width ) ( decltype(ircd::fmt::spec::precision), precision ) ( decltype(ircd::fmt::spec::name), name ) ) #pragma GCC visibility pop /// The format string parser grammar. namespace ircd::fmt::parser { template struct [[gnu::visibility("internal")]] rule :qi::rule { using qi::rule::rule; }; const expr specsym { lit(SPECIFIER) ,"format specifier" }; const expr specterm { lit(SPECIFIER_TERMINATOR) ,"specifier termination" }; const expr name { raw[repeat(1,14)[char_("A-Za-z")]] ,"specifier name" }; const rule spec { (specsym >> !specsym) >> -(char_('+') | char_('-')) >> (-char_('0') | attr(' ')) >> -ushort_ >> -(lit('.') >> ushort_) >> name >> -specterm, "specifier" }; } /// A format specifier handler module. This allows a new "%foo" to be defined /// with custom handling by overriding. This abstraction is inserted into a /// mapping key'ed by the supplied names leading to an instance of this. /// class [[gnu::visibility("hidden")]] ircd::fmt::specifier { static std::map> registry; std::set names; public: virtual bool operator()(char *&out, const size_t &max, const spec &, const arg &) const = 0; specifier(const std::initializer_list &names); specifier(const std::string &name); virtual ~specifier() noexcept; static bool exists(const string_view &name); static const specifier &at(const string_view &name); }; [[clang::always_destroy]] decltype(ircd::fmt::specifier::registry) ircd::fmt::specifier::registry; struct [[gnu::visibility("hidden")]] ircd::fmt::string_specifier :specifier { static const std::tuple < const char *, std::string, std::string_view, ircd::string_view, ircd::json::string, ircd::json::object, ircd::json::array > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::string_specifier { "s"s }; decltype(ircd::fmt::string_specifier::types) ircd::fmt::string_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::bool_specifier :specifier { static const std::tuple < bool, char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::bool_specifier { { "b"s } }; decltype(ircd::fmt::bool_specifier::types) ircd::fmt::bool_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::signed_specifier :specifier { static const std::tuple < bool, char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::signed_specifier { { "d"s, "ld"s, "zd"s } }; decltype(ircd::fmt::signed_specifier::types) ircd::fmt::signed_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::unsigned_specifier :specifier { static const std::tuple < bool, char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::unsigned_specifier { { "u"s, "lu"s, "zu"s } }; struct [[gnu::visibility("hidden")]] ircd::fmt::hex_lowercase_specifier :specifier { static const std::tuple < bool, char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::hex_lowercase_specifier { { "x"s, "lx"s } }; decltype(ircd::fmt::hex_lowercase_specifier::types) ircd::fmt::hex_lowercase_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::hex_uppercase_specifier :specifier { static const std::tuple < bool, char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::hex_uppercase_specifier { { "X"s, "lX"s } }; decltype(ircd::fmt::hex_uppercase_specifier::types) ircd::fmt::hex_uppercase_specifier::types; decltype(ircd::fmt::unsigned_specifier::types) ircd::fmt::unsigned_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::float_specifier :specifier { static const std::tuple < char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, float, double, long double > types; bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::float_specifier { { "f"s, "lf"s } }; decltype(ircd::fmt::float_specifier::types) ircd::fmt::float_specifier::types; struct [[gnu::visibility("hidden")]] ircd::fmt::char_specifier :specifier { bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::char_specifier { "c"s }; struct [[gnu::visibility("hidden")]] ircd::fmt::pointer_specifier :specifier { bool operator()(char *&out, const size_t &max, const spec &, const arg &val) const override; using specifier::specifier; } const ircd::fmt::pointer_specifier { "p"s }; // // snprintf::snprintf // [[gnu::visibility("protected")]] ircd::fmt::snprintf::snprintf(internal_t, const mutable_buffer &out, const string_view &fmt, const va_rtti &v) try :out{out} ,fmt{[&fmt] { // start the member fmt variable at the first specifier (or end) const auto pos(fmt.find(SPECIFIER)); return pos != fmt.npos? fmt.substr(pos): string_view{}; }()} ,idx{0} { // If out has no size we have nothing to do, not even null terminate it. if(unlikely(empty(out))) return; // If fmt has no specifiers then we can just copy the fmt as best as // possible to the out buffer. if(empty(this->fmt)) { append(fmt); return; } // Copy everything from fmt up to the first specifier. assert(data(this->fmt) >= data(fmt)); append(string_view(data(fmt), data(this->fmt))); // Iterate auto it(begin(v)); for(size_t i(0); i < v.size() && !finished(); ++it, i++) { const void *const &ptr(get<0>(*it)); const std::type_index type(*get<1>(*it)); argument(std::make_tuple(ptr, type)); } // Ensure null termination if out buffer is non-empty. assert(size(this->out) > 0); assert(this->out.remaining()); copy(mutable_buffer(this->out), '\0'); } catch(const std::out_of_range &e) { throw invalid_format { "Format string requires more than %zu arguments.", v.size() }; } [[gnu::visibility("hidden")]] void ircd::fmt::snprintf::argument(const arg &val) { // The format string's front pointer is sitting on the specifier '%' // waiting to be parsed now. fmt::spec spec; auto &start(begin(this->fmt)); const auto &stop(end(this->fmt)); if(ircd::parse(start, stop, parser::spec, spec)) handle_specifier(this->out, idx++, spec, val); string_view fmt { start, stop }; if(size(fmt) >= 2 && fmt[0] == SPECIFIER && fmt[1] == SPECIFIER) { append({&SPECIFIER, 1}); consume(this->fmt, 2); fmt = string_view { start, stop }; } const auto nextpos { fmt.find(SPECIFIER) }; const string_view leg { fmt.substr(0, nextpos) }; append(leg); consume(this->fmt, size(leg)); } [[gnu::visibility("hidden")]] void ircd::fmt::snprintf::append(const string_view &src) { out([&src](const mutable_buffer &buf) { return strlcpy(buf, src); }); } [[gnu::visibility("hidden")]] size_t ircd::fmt::snprintf::remaining() const noexcept { return out.remaining()? out.remaining() - 1: 0; } [[gnu::visibility("hidden")]] bool ircd::fmt::snprintf::finished() const noexcept { return empty(fmt) || !remaining(); } // // fmt::specifier // ircd::fmt::specifier::specifier(const std::string &name) :specifier{{name}} { } ircd::fmt::specifier::specifier(const std::initializer_list &names) :names{names} { for(const auto &name : this->names) if(exists(name)) throw error { "Specifier '%s' already registered\n", name }; for(const auto &name : this->names) registry.emplace(name, this); } ircd::fmt::specifier::~specifier() noexcept { for(const auto &name : names) registry.erase(name); } bool ircd::fmt::specifier::exists(const string_view &name) { return registry.count(name); } const ircd::fmt::specifier & ircd::fmt::specifier::at(const string_view &name) { return *registry.at(name); } // // Utils // void ircd::fmt::handle_specifier(mutable_buffer &out, const uint &idx, const spec &spec, const arg &val) try { auto &outp(std::get<0>(out)); assert(size(out)); const size_t max { size(out) - 1 // Leave room for null byte for later. }; assert(spec.name); const auto &type(get<1>(val)); const auto &handler(specifier::at(spec.name)); if(unlikely(!handler(outp, max, spec, val))) throw invalid_type { "`%s' (%s) for format specifier '%s' for argument #%u", demangle(type.name()), type.name(), spec.name, idx }; } catch(const std::out_of_range &e) { throw invalid_format { "Unhandled specifier `%s' for argument #%u in format string", spec.name, idx }; } catch(const illegal &e) { throw illegal { "Specifier `%s' for argument #%u: %s", spec.name, idx, e.what() }; } template bool ircd::fmt::visit_type(const arg &val, lambda&& closure) { const auto &ptr(get<0>(val)); const auto &type(get<1>(val)); return type == typeid(T)? closure(*static_cast(ptr)) : false; } template bool ircd::fmt::generate(mutable_buffer &out, gen&& g, attr&&... a) { constexpr bool truncation { true }; return ircd::generate(out, std::forward(g), std::forward(a)...); } // // Handlers // bool ircd::fmt::pointer_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { using karma::eps; static const auto throw_illegal{[] { throw illegal { "Not a pointer" }; }}; struct generator :karma::grammar { karma::rule rule { lit("0x") << karma::hex }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto &ep { eps[throw_illegal] }; const auto &ptr(get<0>(val)); const auto &type(get<1>(val)); const void *const p { *static_cast(ptr) }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, uintptr_t(p)); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, uintptr_t(p)); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, uintptr_t(p)); } out = data(buf); return ret; } bool ircd::fmt::char_specifier::operator()(char *&out, const size_t &max, const spec &, const arg &val) const { using karma::eps; static const auto throw_illegal{[] { throw illegal { "Not a printable character" }; }}; struct generator :karma::grammar { karma::rule printable { karma::print ,"character" }; generator(): generator::base_type{printable} {} } static const generator; const auto &ptr(get<0>(val)); const auto &type(get<1>(val)); if(type == typeid(const char)) { mutable_buffer buf { out, max }; const auto &c(*static_cast(ptr)); fmt::generate(buf, generator | eps[throw_illegal], c); out = data(buf); return true; } else return false; } bool ircd::fmt::bool_specifier::operator()(char *&out, const size_t &max, const spec &, const arg &val) const { using karma::eps; static const auto throw_illegal{[] { throw illegal { "Failed to print signed value" }; }}; const auto closure([&](const bool &boolean) { struct generator :karma::grammar { karma::rule rule { karma::bool_ ,"boolean" }; generator(): generator::base_type{rule} {} } static const generator; mutable_buffer buf { out, max }; const auto ret { fmt::generate(buf, generator | eps[throw_illegal], boolean) }; out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } bool ircd::fmt::signed_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { static const auto throw_illegal{[] { throw illegal { "Failed to print signed value" }; }}; const auto closure([&out, &max, &spec, &val] (const long &integer) { using karma::long_; struct generator :karma::grammar { karma::rule rule { long_ ,"signed long integer" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto &ep { eps[throw_illegal] }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, integer); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } bool ircd::fmt::unsigned_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { static const auto throw_illegal{[] { throw illegal { "Failed to print unsigned value" }; }}; const auto closure([&out, &max, &spec, &val] (const ulong &integer) { using karma::ulong_; struct generator :karma::grammar { karma::rule rule { ulong_ ,"unsigned long integer" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto &ep { eps[throw_illegal] }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, integer); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } bool ircd::fmt::hex_lowercase_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { static const auto throw_illegal{[] { throw illegal { "Failed to print hexadecimal value" }; }}; const auto closure([&](const ulong &integer) { struct generator :karma::grammar { karma::rule rule { karma::lower[karma::hex] ,"unsigned lowercase hexadecimal" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto &ep { eps[throw_illegal] }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, integer); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } bool ircd::fmt::hex_uppercase_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { static const auto throw_illegal{[] { throw illegal { "Failed to print hexadecimal value" }; }}; const auto closure([&](const ulong &integer) { struct generator :karma::grammar { karma::rule rule { karma::upper[karma::hex] ,"unsigned uppercase hexadecimal" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto &ep { eps[throw_illegal] }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, integer); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, integer); } out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } //TODO: note long double is narrowed to double for now otherwise //TODO: valgrind loops somewhere in here and eats all the system's RAM. bool ircd::fmt::float_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { static const auto throw_illegal{[] { throw illegal { "Failed to print floating point value" }; }}; thread_local uint _precision_; _precision_ = spec.precision; const auto closure([&](const double &floating) { using karma::double_; struct generator :karma::grammar { struct policy :karma::real_policies { static uint precision(const double &) { return _precision_; } static bool trailing_zeros(const double &) { return _precision_ > 0; } static int floatfield(const double &) { return _precision_ > 0? fmtflags::fixed: fmtflags::scientific; } }; karma::rule rule { karma::real_generator() ,"floating point real" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[rule] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[rule] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[rule] ,"center aligned" }; generator(): generator::base_type{rule} {} } static const generator; static const auto ep { eps[throw_illegal] }; bool ret; mutable_buffer buf { out, max }; if(!spec.width) ret = fmt::generate(buf, generator | ep, floating); else if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, floating); } else { const auto &g(generator.aligned_right(spec.width, spec.pad)); ret = fmt::generate(buf, g | ep, floating); } out = data(buf); return ret; }); return test(types, [&](const auto type) { return visit_type(val, closure); }); } bool ircd::fmt::string_specifier::operator()(char *&out, const size_t &max, const spec &spec, const arg &val) const { using karma::char_; using karma::eps; using karma::unused_type; static const auto throw_illegal{[] { throw illegal { "Not a printable string" }; }}; struct generator :karma::grammar { karma::rule string { *(~ascii::cntrl) ,"string" }; _r1_type width; _r2_type pad; karma::rule aligned_left { karma::left_align(width, pad)[string] ,"left aligned" }; karma::rule aligned_right { karma::right_align(width, pad)[string] ,"right aligned" }; karma::rule aligned_center { karma::center(width, pad)[string] ,"center aligned" }; generator() :generator::base_type{string} {} } static const generator; static const auto ep { eps[throw_illegal] }; if(!spec.width) return generate_string(out, max, generator | ep, val); if(spec.sign == '-') { const auto &g(generator.aligned_left(spec.width, spec.pad)); return generate_string(out, max, g | ep, val); } const auto &g(generator.aligned_right(spec.width, spec.pad)); return generate_string(out, max, g | ep, val); } template bool ircd::fmt::generate_string(char *&out, const size_t &max, generator&& gen, const arg &val) { using karma::eps; bool ret; mutable_buffer buf { out, max }; const auto &ptr(get<0>(val)); const auto &type(get<1>(val)); if(type == typeid(ircd::string_view) || type == typeid(ircd::json::string) || type == typeid(ircd::json::object) || type == typeid(ircd::json::array)) { const auto &str(*static_cast(ptr)); ret = fmt::generate(buf, std::forward(gen), str); } else if(type == typeid(std::string_view)) { const auto &str(*static_cast(ptr)); ret = fmt::generate(buf, std::forward(gen), str); } else if(type == typeid(std::string)) { const auto &str(*static_cast(ptr)); ret = fmt::generate(buf, std::forward(gen), string_view{str}); } else if(type == typeid(const char *)) { const char *const &str{*static_cast(ptr)}; ret = fmt::generate(buf, std::forward(gen), string_view{str}); } else { // This for string literals which have unique array types depending on their size. // There is no reasonable way to match them. The best that can be hoped for is the // grammar will fail gracefully (most of the time) or not print something bogus when // it happens to be legal. const auto &str(static_cast(ptr)); ret = fmt::generate(buf, std::forward(gen), string_view{str}); } out = data(buf); return ret; }