diff options
author | Sven Göthel <[email protected]> | 2024-09-14 06:10:35 +0200 |
---|---|---|
committer | Sven Göthel <[email protected]> | 2024-09-14 06:10:35 +0200 |
commit | bc3df78443ad99c8fa76a6b58d07c4336255e323 (patch) | |
tree | 6c8461d49e1c2fbd996c6835ed7e92abf1e237ff | |
parent | b8f7f12979bc35d7cab0905ae83040847721c86a (diff) |
jau::cfmt: cleanup and add robusteness, adding alternative recursive version
-rw-r--r-- | include/jau/string_cfmt.hpp | 55 | ||||
-rw-r--r-- | include/jau/string_util.hpp | 5 | ||||
-rw-r--r-- | test/string_cfmt2.hpp | 803 | ||||
-rw-r--r-- | test/test_stringfmt01.cpp | 152 |
4 files changed, 952 insertions, 63 deletions
diff --git a/include/jau/string_cfmt.hpp b/include/jau/string_cfmt.hpp index 9574c6a..c097b70 100644 --- a/include/jau/string_cfmt.hpp +++ b/include/jau/string_cfmt.hpp @@ -274,7 +274,7 @@ namespace jau::cfmt { * @return true if no error _and_ not complete, i.e. further calls with subsequent parameter required. Otherwise parsing is done due to error or completeness. */ template <typename T> - constexpr bool parseOne(PResult &pc) const { + constexpr bool parseOne(PResult &pc) const noexcept { if( !pc.hasNext() ) { return false; // done or error } @@ -306,7 +306,7 @@ namespace jau::cfmt { pc.state = pstate_t::precision; if( c == '.' ) { if( !parsePrecision<T>(pc, c) ) { - return !pc.error(); // error or continue with next argument for same conversion -> field_width + return !pc.error(); // error or continue with next argument for same conversion -> precision } } } @@ -398,6 +398,7 @@ namespace jau::cfmt { return true; // continue with current argument } + /* parse length modifier, returns true if parsing can continue or false on error. */ constexpr bool parseLengthMods(PResult &pc, char &c) const noexcept { if( 'h' == c ) { if( !pc.nextSymbol(c) ) { return false; } @@ -441,28 +442,16 @@ namespace jau::cfmt { case '%': case 'c': case 's': - if( !parseStringFmtSpec<T>(pc, fmt_literal) ) { - return false; - } - break; + return parseStringFmtSpec<T>(pc, fmt_literal); case 'p': - if( !parseAPointerFmtSpec<T>(pc) ) { - return false; - } - break; + return parseAPointerFmtSpec<T>(pc); case 'd': - if( !parseSignedFmtSpec<T>(pc) ) { - return false; - } - break; + return parseSignedFmtSpec<T>(pc); case 'o': case 'x': case 'X': case 'u': - if( !parseUnsignedFmtSpec<T>(pc, fmt_literal) ) { - return false; - } - break; + return parseUnsignedFmtSpec<T>(pc, fmt_literal); case 'f': case 'e': case 'E': @@ -470,15 +459,11 @@ namespace jau::cfmt { case 'A': case 'g': case 'G': - if( !parseFloatFmtSpec<T>(pc, fmt_literal) ) { - return false; - } - break; + return parseFloatFmtSpec<T>(pc, fmt_literal); default: pc.setError(__LINE__); return false; } // switch( fmt_literal ) - return true; } constexpr char unaliasFmtSpec(const char fmt_literal) const noexcept { @@ -491,7 +476,6 @@ namespace jau::cfmt { template <typename T> constexpr bool parseStringFmtSpec(PResult &pc, const char fmt_literal) const noexcept { - pc.length_mod = plength_t::none; if constexpr( std::is_same_v<no_type_t, T> ) { pc.setError(__LINE__); return false; @@ -546,7 +530,9 @@ namespace jau::cfmt { return false; } break; - default: break; + default: + pc.setError(__LINE__); + return false; } return true; } @@ -754,7 +740,7 @@ namespace jau::cfmt { * @see @ref jau_cfmt_header */ template <typename... Targs> - constexpr bool check(std::string_view fmt, const Targs &...) { + constexpr bool check(const std::string_view fmt, const Targs &...) noexcept { PResult ctx(fmt); constexpr const impl::Parser p; if constexpr( 0 < sizeof...(Targs) ) { @@ -774,7 +760,18 @@ namespace jau::cfmt { * @see @ref jau_cfmt_header */ template <typename... Targs> - constexpr bool check2(std::string_view fmt) { + constexpr bool check2(const std::string_view fmt) noexcept { + PResult ctx(fmt); + constexpr const impl::Parser p; + if constexpr( 0 < sizeof...(Targs) ) { + ((p.template parseOne<Targs>(ctx)), ...); + } + p.template parseOne<impl::no_type_t>(ctx); + return !ctx.error(); + } + + template <typename StrView, typename... Targs> + constexpr bool check3(StrView fmt) noexcept { PResult ctx(fmt); constexpr const impl::Parser p; if constexpr( 0 < sizeof...(Targs) ) { @@ -796,7 +793,7 @@ namespace jau::cfmt { * @see @ref jau_cfmt_header */ template <typename... Targs> - constexpr PResult checkR(std::string_view fmt, const Targs &...) { + constexpr PResult checkR(const std::string_view fmt, const Targs &...) noexcept { PResult ctx(fmt); constexpr const impl::Parser p; if constexpr( 0 < sizeof...(Targs) ) { @@ -817,7 +814,7 @@ namespace jau::cfmt { * @see @ref jau_cfmt_header */ template <typename... Targs> - constexpr PResult checkR2(std::string_view fmt) { + constexpr PResult checkR2(const std::string_view fmt) noexcept { PResult ctx(fmt); constexpr const impl::Parser p; if constexpr( 0 < sizeof...(Targs) ) { diff --git a/include/jau/string_util.hpp b/include/jau/string_util.hpp index a9844cd..7520166 100644 --- a/include/jau/string_util.hpp +++ b/include/jau/string_util.hpp @@ -29,6 +29,7 @@ #include <cstring> #include <string> #include <cstdarg> +#include <string_view> #include <type_traits> #include <vector> @@ -433,6 +434,10 @@ namespace jau { } // namespace jau +#define jau_format_string_static(...) \ + jau::format_string(__VA_ARGS__); \ + static_assert( 0 <= jau::cfmt::checkR(__VA_ARGS__).argCount() ); // compile time validation! + /** \example test_intdecstring01.cpp * This C++ unit test validates the jau::to_decstring implementation */ diff --git a/test/string_cfmt2.hpp b/test/string_cfmt2.hpp new file mode 100644 index 0000000..6f2ff49 --- /dev/null +++ b/test/string_cfmt2.hpp @@ -0,0 +1,803 @@ +/** + * Copyright the Collabora Online contributors. + * Copyright Gothel Software e.K. + * + * *** + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * *** + * + * SPDX-License-Identifier: MIT + * + * This Source Code Form is subject to the terms of the MIT License + * If a copy of the MIT was not distributed with this + * file, You can obtain one at https://opensource.org/license/mit/. + * + */ + +#ifndef JAU_STRING_CFMT2_HPP_ +#define JAU_STRING_CFMT2_HPP_ + +#include <bits/types/wint_t.h> +#include <sys/types.h> +#include <cassert> +#include <cerrno> +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <limits> +#include <string> +#include <string_view> +#include <type_traits> +#include <iostream> + +#include <jau/byte_util.hpp> +#include <jau/cpp_lang_util.hpp> +#include <jau/packed_attribute.hpp> +#include <jau/type_traits_queries.hpp> + +#include <jau/cpp_pragma.hpp> + +/** + * @anchor jau_cfmt_header + * ## `snprintf` argument type checker `jau::cfmt` + * + * ### Features + * - jau::cfmt::check() provides strict type matching of arguments against the format string. + * - Have jau::cfmt::check() to be passed _before_ using std::snprintf(), + * removing safety concerns of the latter and benefit from its formatting and performance. + * - Follows [C++ Reference](https://en.cppreference.com/w/cpp/io/c/fprintf) + * + * ### Type Conversion + * Implementation follows type conversion rules as described + * in [Variadic Default Conversion](https://en.cppreference.com/w/cpp/language/variadic_arguments#Default_conversions) + * - float to double promotion + * - bool, char, short, and unscoped enumerations are converted to int or wider integer types + * as well as in [va_arg](https://en.cppreference.com/w/cpp/utility/variadic/va_arg) + * - ignore signed/unsigned type differences for integral types + * - void pointer tolerance + * + * ### Implementation Details + * - Validates arguments against format string at compile time or runtime, + * depending whether passed arguments are of constexpr nature (compile time). + * - Written in C++20 using template argument pack w/ save argument type checks + * - Written as constexpr, capable to be utilized at compile-time. + * + * #### Supported Conversion Specifiers and Data Types + * + * The following conversion specifiers are supported: + * - `c`, `s`, `d`, `o`, `x`, `X`, `u`, `f`, `e`, `E`, `a`, `A`, `g`, `G`, `p` + * - Their synonyms + * - `i` -> `d` + * - `F` -> `f` + * - Flags `-`, `+`, ` `, `0` and `#` + * - Asterisk `*` for field width and precision + * + * The following length modifiers are supported where allowed + * - `hh` [unsigned] char + * - `h` [unsigned] short + * - `l` [unsigned] long + * - `ll` [unsigned] long long + * - 'j' uintmax_t or intmax_t + * - 'z' size_t or ssize_t + * - 't' ptrdiff_t + * - 'L' long double + * + * See [C++ Reference](https://en.cppreference.com/w/cpp/io/c/fprintf) for details. + * + * ### Further Documentation + * - [C++ Reference](https://en.cppreference.com/w/cpp/io/c/fprintf) + * - [Linux snprintf(3) man page](https://www.man7.org/linux/man-pages/man3/snprintf.3p.html) + * - [FreeBSD snprintf(3) man page](https://man.freebsd.org/cgi/man.cgi?snprintf(3)) + */ +namespace jau::cfmt2 { + + /** \addtogroup StringUtils + * + * @{ + */ + + enum class pstate_t { + error, + outside, + start, + field_width, + precision + }; + constexpr static const char *to_string(pstate_t s) noexcept { + switch( s ) { + case pstate_t::outside: return "outside"; + case pstate_t::start: return "start"; + case pstate_t::field_width: return "width"; + case pstate_t::precision: return "precision"; + default: return "error"; + } + } + enum class plength_t { + hh, + h, + none, + l, + ll, + j, + z, + t, + L + }; + constexpr static const char *to_string(plength_t s) noexcept { + switch( s ) { + case plength_t::hh: return "hh"; + case plength_t::h: return "h"; + case plength_t::none: return ""; + case plength_t::l: return "l"; + case plength_t::ll: return "ll"; + case plength_t::j: return "j"; + case plength_t::z: return "z"; + case plength_t::t: return "t"; + case plength_t::L: return "L"; + default: return "n/a"; + } + } + + static constexpr bool isDigit(const char c) noexcept { return '0' <= c && c <= '9'; } + + namespace impl { + template <typename T> + class Parser; // fwd + } + + struct PResult { + const std::string_view fmt; + const size_t pos; + const ssize_t arg_count; + const int line; + const pstate_t state; + const plength_t length_mod; + const bool precision_set; + + template <size_t N> + constexpr PResult(const char (&fmt_)[N]) noexcept + : fmt(fmt_, N), pos(0), arg_count(0), line(0), state(pstate_t::outside), length_mod(plength_t::none), precision_set(false) { } + + constexpr PResult(const std::string_view fmt_) noexcept + : fmt(fmt_), pos(0), arg_count(0), line(0), state(pstate_t::outside), length_mod(plength_t::none), precision_set(false) { } + + constexpr PResult(const PResult &pre) noexcept = default; + + constexpr PResult &operator=(const PResult &x) noexcept = delete; + + constexpr bool hasNext() const noexcept { + return !error() && pos < fmt.length() - 1; + } + + constexpr ssize_t argCount() const noexcept { return arg_count; } + constexpr bool error() const noexcept { return pstate_t::error == state; } + constexpr char sym() const noexcept { return fmt[pos]; } + + std::string toString() const { + const char c = pos < fmt.length() ? fmt[pos] : '@'; + std::string s = "args "; + s.append(std::to_string(arg_count)) + .append(", state ") + .append(to_string(state)) + .append(", line ") + .append(std::to_string(line)) + .append(", pos ") + .append(std::to_string(pos)) + .append(", char `") + .append(std::string(1, c)) + .append("`, length `") + .append(to_string(length_mod)) + .append("`, precision ") + .append(std::to_string(precision_set)) + .append(", fmt `") + .append(fmt) + .append("`"); + return s; + } + + private: + template <typename T> + friend class impl::Parser; + + constexpr PResult(const PResult &pre, size_t pos2) noexcept + : fmt(pre.fmt), pos(pos2), arg_count(pre.arg_count), line(pre.line), state(pre.state), length_mod(pre.length_mod), precision_set(pre.precision_set) {} + + constexpr PResult(const PResult &pre, size_t pos2, ssize_t arg_count2) noexcept + : fmt(pre.fmt), pos(pos2), arg_count(arg_count2), line(pre.line), state(pre.state), length_mod(pre.length_mod), precision_set(pre.precision_set) {} + + constexpr PResult(const PResult &pre, pstate_t state2) noexcept + : fmt(pre.fmt), pos(pre.pos), arg_count(pre.arg_count), line(pre.line), state(state2), length_mod(pre.length_mod), precision_set(pre.precision_set) {} + + constexpr PResult(const PResult &pre, pstate_t state2, size_t pos2) noexcept + : fmt(pre.fmt), pos(pos2), arg_count(pre.arg_count), line(pre.line), state(state2), length_mod(pre.length_mod), precision_set(pre.precision_set) {} + + constexpr PResult(const PResult &pre, pstate_t state2, size_t pos2, ssize_t arg_count2, int line2) noexcept + : fmt(pre.fmt), pos(pos2), arg_count(arg_count2), line(line2), state(state2), length_mod(pre.length_mod), precision_set(pre.precision_set) {} + + constexpr PResult(const PResult &pre, plength_t length_mod2, bool precision_set2) noexcept + : fmt(pre.fmt), pos(pre.pos), arg_count(pre.arg_count), line(pre.line), state(pre.state), length_mod(length_mod2), precision_set(precision_set2) {} + + constexpr PResult nextSymbol() const noexcept { + if( pos < fmt.length() - 1 ) { + return PResult(*this, pos+1); + } else { + return *this; + } + } + constexpr PResult toConversion() const noexcept { + if( pstate_t::outside != state ) { + return PResult(*this); // inside conversion specifier + } else if( fmt[pos] == '%' ) { + return PResult(*this, pstate_t::start, pos); // just at start of conversion specifier + } else { + // seek next conversion specifier + const size_t q = fmt.find('%', pos + 1); + if( q == std::string::npos ) { + // no conversion specifier found + return PResult(*this, fmt.length()); + } else { + // new conversion specifier found + return PResult(*this, pstate_t::start, q); + } + } + } + + constexpr PResult setError(int l) const noexcept { + ssize_t arg_count2; + if( 0 == arg_count ) { + arg_count2 = std::numeric_limits<ssize_t>::min(); + } else if( 0 < arg_count ) { + arg_count2 = arg_count * -1; + } else { + arg_count2 = arg_count; + } + return PResult(*this, pstate_t::error, pos, arg_count2, l); + } + }; + + inline std::ostream &operator<<(std::ostream &out, const PResult &pc) { + out << pc.toString(); + return out; + } + + namespace impl { + + static constexpr bool verbose_error = true; + + enum class no_type_t {}; + + template <typename T> + class Parser { + public: + constexpr Parser() noexcept = delete; + + /** + * Parse the given argument against the current conversion specifier of the format string. + * + * Multiple rounds of parsing calls might be required, each passing the next argument or null. + * + * Parsing is completed when method returns false, either signaling an error() or completion. + * + * @tparam T The type of the given argument + * @return true if no error _and_ not complete, i.e. further calls with subsequent parameter required. Otherwise parsing is done due to error or completeness. + */ + static constexpr const PResult parseOne(const PResult pc) noexcept { + if( !pc.hasNext() ) { + return pc; // done or error + } + + const PResult pc2 = pc.toConversion(); + if( !pc2.hasNext() ) { + return pc2; // done + } + + // pstate_t::outside != _state + + /* skip '%' or previous `*` */ + const PResult pc3 = pc2.nextSymbol(); + + if( pstate_t::start == pc3.state ) { + const PResult pc4 = parseFlags(PResult(pc3, pstate_t::field_width)); + + /* parse field width */ + bool next_arg = false; + const PResult pc5 = parseFieldWidth(pc4, next_arg); + if( next_arg || pc5.error() ) { + return pc5; // error or continue with next argument for same conversion -> field_width + } + return parseP2(pc5); + } else { + return parseP2(pc3); + } + } + + private: + + static constexpr const PResult parseP2(const PResult pc) noexcept { + if( pstate_t::field_width == pc.state ) { + /* parse precision */ + const PResult pc2 = PResult(pc, pstate_t::precision); + if( pc2.sym() == '.' ) { + const PResult pc3 = PResult(pc2, pc2.length_mod, true); + bool next_arg = false; + const PResult pc4 = parsePrecision(pc3, next_arg); + if( next_arg || pc4.error() ) { + return pc4; // error or continue with next argument for same conversion -> precision + } + return parseP3(pc4); + } else { + return parseP3(pc2); + } + } else { + return parseP3(pc); + } + } + + static constexpr const PResult parseP3(const PResult pc) noexcept { + const PResult pc2 = parseLengthMods(pc); + if( pc2.error() ) { + return pc2; // error + } + + const PResult pc3 = parseFmtSpec(pc2); + if( pc3.error() ) { + return pc3; // error + } + + // next conversion specifier + const PResult pc4 = PResult(pc3, pstate_t::outside); + if( !pc4.hasNext() ) { + return pc4; // done + } + return PResult(pc4.nextSymbol(), plength_t::none, false); // clear length_mod and precision_set + } + + static constexpr const PResult parseFlags(const PResult pc) noexcept { + switch( pc.sym() ) { + case '0': break; + case '-': break; + case '+': break; + case ' ': break; + case '#': break; + case '\'': break; + default: return pc; // done, not a flag + } + if( pc.hasNext() ) { + return parseFlags(pc.nextSymbol()); + } else { + return pc; + } + } + + static constexpr const PResult parseDigit(const PResult pc) noexcept { + if( !pc.hasNext() || !isDigit(pc.sym()) ) { return pc; } + return parseDigit( pc.nextSymbol() ); + } + + /* parse field width, returns true if parsing can continue or false if next argument is required or error */ + static constexpr const PResult parseFieldWidth(const PResult pc, bool& next_arg) noexcept { + next_arg = false; + if( pc.sym() == '*' ) { + if( !pc.hasNext() ) { return pc; } + const PResult pc2 = pc.nextSymbol(); + + using U = std::remove_cv_t<T>; + + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc2.setError(__LINE__); + } + const PResult pc3 = PResult(pc2, pc.arg_count+1); + if constexpr( !std::is_same_v<int, U> ) { + return pc3.setError(__LINE__); + } + next_arg = true; + return pc3; // next argument is required + } else { + // continue with current argument + return parseDigit(pc); + } + } + + /* parse precision, returns true if parsing can continue or false if next argument is required or error */ + static constexpr const PResult parsePrecision(const PResult pc, bool &next_arg) noexcept { + next_arg = false; + if( !pc.hasNext() ) { return pc; } + const PResult pc2 = pc.nextSymbol(); + const char c = pc.fmt[pc.pos]; + if( c == '*' ) { + if( !pc2.hasNext() ) { return pc2; } + const PResult pc3 = pc2.nextSymbol(); + + using U = std::remove_cv_t<T>; + + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc3.setError(__LINE__); + } + const PResult pc4 = PResult(pc3, pc.arg_count+1); + if constexpr( !std::is_same_v<int, U> ) { + return pc4.setError(__LINE__); + } + next_arg = true; + return pc4; // next argument is required + } else { + // continue with current argument + return parseDigit(pc2); + } + } + + static constexpr const PResult parseLengthMods(const PResult pc) noexcept { + const char sym = pc.sym(); + if( 'h' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + const PResult pc2 = pc.nextSymbol(); + if( 'h' == pc2.sym() ) { + if( !pc2.hasNext() ) { return pc2.setError(__LINE__); } + return PResult(pc2.nextSymbol(), plength_t::hh, pc2.precision_set); + } else { + return PResult(pc2, plength_t::h, pc2.precision_set); + } + } else if( 'l' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + const PResult pc2 = pc.nextSymbol(); + if( 'l' == pc2.sym() ) { + if( !pc2.hasNext() ) { return pc2.setError(__LINE__); } + return PResult(pc2.nextSymbol(), plength_t::ll, pc2.precision_set); + } else { + return PResult(pc2, plength_t::l, pc2.precision_set); + } + } else if( 'j' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + return PResult(pc.nextSymbol(), plength_t::j, pc.precision_set); + } else if( 'z' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + return PResult(pc.nextSymbol(), plength_t::z, pc.precision_set); + } else if( 't' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + return PResult(pc.nextSymbol(), plength_t::t, pc.precision_set); + } else if( 'L' == sym ) { + if( !pc.hasNext() ) { return pc.setError(__LINE__); } + return PResult(pc.nextSymbol(), plength_t::L, pc.precision_set); + } else { + return PResult(pc, plength_t::none, pc.precision_set); + } + } + + static constexpr const PResult parseFmtSpec(const PResult pc) noexcept { + const char fmt_literal = unaliasFmtSpec( pc.sym() ); + + switch( fmt_literal ) { + case '%': + case 'c': + case 's': + return parseStringFmtSpec(pc, fmt_literal); + case 'p': + return parseAPointerFmtSpec(pc); + case 'd': + return parseSignedFmtSpec(pc); + case 'o': + case 'x': + case 'X': + case 'u': + return parseUnsignedFmtSpec(pc, fmt_literal); + case 'f': + case 'e': + case 'E': + case 'a': + case 'A': + case 'g': + case 'G': + return parseFloatFmtSpec(pc, fmt_literal); + default: + return pc.setError(__LINE__); + } // switch( fmt_literal ) + } + + static constexpr char unaliasFmtSpec(const char fmt_literal) noexcept { + switch( fmt_literal ) { + case 'i': return 'd'; + case 'F': return 'f'; + default: return fmt_literal; + } + } + + static constexpr const PResult parseStringFmtSpec(const PResult pc, const char fmt_literal) noexcept { + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc.setError(__LINE__); + } + switch( fmt_literal ) { + case '%': + break; + case 'c': { + const PResult pc2 = PResult(pc, pc.pos, pc.arg_count+1); + using U = std::remove_cv_t<T>; + switch( pc2.length_mod ) { + case plength_t::none: + if constexpr( !std::is_same_v<char, U> || + !std::is_same_v<int, U> ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::l: + if constexpr( !std::is_same_v<wchar_t, U> || + !std::is_same_v<wint_t, U> ) { + return pc2.setError(__LINE__); + } + break; + default: + return pc2.setError(__LINE__); + } + return pc2; + } + case 's': { + const PResult pc2 = PResult(pc, pc.pos, pc.arg_count+1); + switch( pc2.length_mod ) { + case plength_t::none: + if constexpr( !std::is_pointer_v<T> || + !std::is_same_v<char, std::remove_cv_t<std::remove_pointer_t<T>>> ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::l: + if constexpr( !std::is_pointer_v<T> || + !std::is_same_v<wchar_t, std::remove_cv_t<std::remove_pointer_t<T>>> ) { + return pc2.setError(__LINE__); + } + break; + default: + return pc2.setError(__LINE__); + } + return pc2; + } + default: return pc.setError(__LINE__); + } + return pc; + } + + static constexpr const PResult parseAPointerFmtSpec(const PResult pc) noexcept { + const PResult pc2 = PResult(pc, plength_t::none, pc.precision_set); + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc2.setError(__LINE__); + } + const PResult pc3 = PResult(pc2, pc2.pos, pc2.arg_count+1); + if constexpr( !std::is_pointer_v<T> ) { + return pc3.setError(__LINE__); + } + return pc3; + } + + static constexpr const PResult parseSignedFmtSpec(const PResult pc) noexcept { + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc.setError(__LINE__); + } + const PResult pc2 = PResult(pc, pc.pos, pc.arg_count+1); + + using U = std::remove_cv_t<T>; + // using U = std::conditional<std::is_integral_v<T> && std::is_unsigned_v<T>, std::make_signed<T>, T>::type; // triggers the instantiating the 'other' case and hence fails + using V = typename std::conditional_t<std::is_integral_v<U> && std::is_unsigned_v<U>, std::make_signed<U>, std::type_identity<U>>::type; // NOLINT + + switch( pc2.length_mod ) { + case plength_t::hh: + if constexpr( !std::is_same_v<char, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(char)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::h: + if constexpr( !std::is_same_v<short, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(short)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::none: + if constexpr( !std::is_same_v<int, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(int)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::l: + if constexpr( !std::is_same_v<long, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(long)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::ll: + if constexpr( !std::is_same_v<long long, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(long long)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::j: + if constexpr( !std::is_same_v<intmax_t, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(intmax_t)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::z: + if constexpr( !std::is_same_v<ssize_t, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(ssize_t)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::t: + if constexpr( !std::is_same_v<ptrdiff_t, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(ptrdiff_t)) ) { + return pc2.setError(__LINE__); + } + break; + default: + return pc2.setError(__LINE__); + } + return pc2; + } + + static constexpr const PResult parseUnsignedFmtSpec(const PResult pc, const char /*fmt_literal*/) noexcept { + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc.setError(__LINE__); + } + const PResult pc2 = PResult(pc, pc.pos, pc.arg_count+1); + + using U = std::remove_cv_t<T>; + // using U = std::conditional_t<std::is_integral_v<T> && std::is_signed_v<T>, std::make_unsigned_t<T>, T>; // triggers the instantiating the 'other' case and hence fails + using V = typename std::conditional_t<std::is_integral_v<U> && std::is_signed_v<U>, std::make_unsigned<U>, std::type_identity<U>>::type; // NOLINT + + switch( pc2.length_mod ) { + case plength_t::hh: + if constexpr( !std::is_same_v<unsigned char, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(unsigned char)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::h: + if constexpr( !std::is_same_v<unsigned short, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(unsigned short)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::none: + if constexpr( !std::is_same_v<unsigned int, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(unsigned int)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::l: + if constexpr( !std::is_same_v<unsigned long, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(unsigned long)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::ll: + if constexpr( !std::is_same_v<unsigned long long, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(unsigned long long)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::j: + if constexpr( !std::is_same_v<uintmax_t, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(uintmax_t)) ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::z: + if constexpr( !std::is_same_v<size_t, V> && (!std::is_integral_v<V> || sizeof(V) > sizeof(size_t)) ) { + return pc2.setError(__LINE__); + } + break; +#if 0 + case plength_t::t: + if constexpr( !std::is_same_v<unsigned ptrdiff_t, U> ) { + return pc2.setError(__LINE__); + } + break; +#endif + default: + return pc2.setError(__LINE__); + } + return pc2; + } + + static constexpr const PResult parseFloatFmtSpec(const PResult pc, const char /*fmt_literal*/) noexcept { + if constexpr( std::is_same_v<no_type_t, T> ) { + return pc.setError(__LINE__); + } + const PResult pc2 = PResult(pc, pc.pos, pc.arg_count+1); + + using U = std::remove_cv_t<T>; + + switch( pc2.length_mod ) { + case plength_t::none: + case plength_t::l: + if constexpr( !std::is_same_v<float, U> && + !std::is_same_v<double, U> ) { + return pc2.setError(__LINE__); + } + break; + case plength_t::L: + if constexpr( !std::is_same_v<float, U> && + !std::is_same_v<double, U> && + !std::is_same_v<long double, U> ) { + } else { + return pc2.setError(__LINE__); + } + break; + default: + return pc2.setError(__LINE__); + } + return pc2; + } + }; + + constexpr const PResult checkRec(const PResult ctx) noexcept { + return Parser<no_type_t>::parseOne(ctx); + } + + template <typename Targ, typename... Tnext> + constexpr const PResult checkRec(const PResult ctx) noexcept { + if constexpr( 0 < sizeof...(Tnext) ) { + return checkRec<Tnext...>( Parser<Targ>::parseOne(ctx) ); + } else { + return Parser<no_type_t>::parseOne( Parser<Targ>::parseOne(ctx) ); + } + } + + } // namespace impl + + /** + * Strict type validation of arguments against the format string. + * + * See @ref jau_cfmt_header for details + * + * @tparam Targs the argument template type pack to be validated against the format string + * @param fmt the snprintf format string + * @param args passed arguments, used for template type deduction only + * @return true if successfully parsed format and arguments, false otherwise. + * @see @ref jau_cfmt_header + */ + template <typename... Targs> + constexpr bool check(const std::string_view fmt, const Targs &...) noexcept { + return !impl::checkRec<Targs...>( PResult(fmt) ).error(); + } + + /** + * Strict type validation of arguments against the format string. + * + * See @ref jau_cfmt_header for details + * + * @tparam Targs the argument template type pack to be validated against the format string + * @param fmt the snprintf format string + * @return true if successfully parsed format and arguments, false otherwise. + * @see @ref jau_cfmt_header + */ + template <typename... Targs> + constexpr bool check2(const std::string_view fmt) noexcept { + return !impl::checkRec<Targs...>( PResult(fmt) ).error(); + } + + /** + * Strict type validation of arguments against the format string. + * + * See @ref jau_cfmt_header for details + * + * @tparam Targs the argument template type pack to be validated against the format string + * @param fmt the snprintf format string + * @param args passed arguments, used for template type deduction only + * @return PContext result object for further inspection. + * @see @ref jau_cfmt_header + */ + template <typename... Targs> + constexpr const PResult checkR(const std::string_view fmt, const Targs &...) noexcept { + return impl::checkRec<Targs...>( PResult(fmt) ); + } + + /** + * Strict type validation of arguments against the format string. + * + * See @ref jau_cfmt_header for details + * + * @tparam Targs the argument template type pack to be validated against the format string + * @param fmt the snprintf format string + * @return PContext result object for further inspection. + * @see @ref jau_cfmt_header + */ + template <typename... Targs> + constexpr const PResult checkR2(const std::string_view fmt) noexcept { + return impl::checkRec<Targs...>( PResult(fmt) ); + } + + /**@}*/ + +} // namespace jau::cfmt2 + +#endif // JAU_STRING_CFMT2_HPP_ diff --git a/test/test_stringfmt01.cpp b/test/test_stringfmt01.cpp index e19ca6c..279d9bb 100644 --- a/test/test_stringfmt01.cpp +++ b/test/test_stringfmt01.cpp @@ -19,6 +19,7 @@ * file, You can obtain one at https://opensource.org/license/mit/. * */ +#include <sys/types.h> #include <cassert> #include <cinttypes> #include <cstdint> @@ -36,28 +37,20 @@ #include <jau/string_cfmt.hpp> #include <jau/string_util.hpp> #include <jau/test/catch2_ext.hpp> +#include <jau/type_traits_queries.hpp> + +#include "string_cfmt2.hpp" #ifdef HAS_STD_FORMAT #include <format> #endif +using namespace std::literals; + using namespace jau::float_literals; using namespace jau::int_literals; -#define format_string_static_assert(...) \ - jau::format_string(__VA_ARGS__); \ - static_assert( 0 <= jau::cfmt::checkR(__VA_ARGS__).argCount() ); // compile time validation! - -#if 0 -// sadly doesn't work .. 'format' not constexpr .. -template <typename... Args> -constexpr std::string format_string_static_assert2(const std::string_view format, const Args &...args) { - static_assert(jau::cfmt::check2<Args...>(format)); - return jau::format_string(format, args...); -} -#endif - template <typename... Args> constexpr std::string format_string000(const std::size_t maxStrLen, const std::string_view format, const Args &...args) { std::string str; @@ -217,6 +210,34 @@ void format_0b() { #endif } +template <typename... Args> +constexpr std::string format_string_static2(const std::string_view fmt, const Args &...) { + // constexpr const std::string format2(format); + // constexpr const bool b = jau::cfmt::check2<Args...>("Haus"sv); + constexpr const bool b = jau::cfmt::check3<Args...>(fmt); + static_assert( b ); + (void)b; + return ""; // jau::format_string_v(1024, format, args...); +} + + +template <typename... Targs> +constexpr jau::cfmt2::PResult check(const std::string_view fmt, const Targs &...) noexcept { + return jau::cfmt2::impl::checkRec<Targs...>( jau::cfmt2::PResult(fmt) ); +} + +template <typename... Targs> +constexpr std::string format_string_static3(const std::string_view format, const Targs &...args) { + // constexpr const jau::cfmt2::PResult ctx2 = jau::cfmt2::impl::checkRec<Targs...>( jau::cfmt2::PResult(format) ); + // static_assert( 0 <= jau::cfmt2::impl::checkRec<Targs...>( jau::cfmt2::PResult(format)).argCount() ); + if( 0 <= jau::cfmt2::impl::checkRec<Targs...>( jau::cfmt2::PResult(format)).argCount() ) { + return jau::format_string_v(1024, format, args...); + } else { + return ""; + } +} + + TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { char buf[1024]; constexpr float fa = 1.123456f, fb = 2.2f; @@ -226,10 +247,51 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { const float *pf = &fa; { + constexpr jau::cfmt2::PResult pr = jau::cfmt2::checkR("lala %d", 2); + std::cerr << "XXX: " << __LINE__ << ": " << pr << std::endl; + static_assert( 0 <= pr.argCount() ); + } + { // 'format_check: %.2f, %2.2f, %zu, %lu, %03d' + constexpr jau::cfmt2::PResult pc = jau::cfmt2::checkR("format_check: %.2f, %2.2f, %zu, %" PRIu64 ", %03d\n", + fa, fb, sz1, sz2, i); + std::cerr << "XXX: " << __LINE__ << ": " << pc << std::endl; + // static_assert( 5 == pc.argCount() ); + REQUIRE(5 == pc.argCount()); + } + { + constexpr jau::cfmt2::PResult pr = jau::cfmt2::checkR("lala %d - end", 2); + std::cerr << "XXX: " << __LINE__ << ": " << pr << std::endl; + // static_assert( 0 <= pr.argCount() ); + REQUIRE(0 <= pr.argCount()); + } + { + constexpr const bool b1 = jau::cfmt::check("lala %d", 2); + static_assert( b1 ); + + constexpr const bool b2 = jau::cfmt::check2<int>("lala %d"); + static_assert( b2 ); + + // constexpr jau::cfmt2::PResult pr1 = check(fmt3, 2); + constexpr jau::cfmt2::PResult pr1 = check("Hello %d", 2); + std::cerr << "XXX: " << __LINE__ << ": " << pr1 << std::endl; + static_assert( 0 <= pr1.argCount() ); + + std::string s3 = format_string_static3("Hello %d", 2); + std::cerr << "XXX: " << __LINE__ << ": " << s3 << std::endl; + REQUIRE( s3.length() > 0 ); + + // constexpr const std::string f2 = "Hello %d"s; + // constexpr const std::string_view f2v = "Hello %d"sv; + // format_string_static2(f2v, (int)2); + // constexpr const std::string_view f3 = "Hello %d"sv; + // constexpr const char f3[] = "Hello %d"; + // format_string_static2("Hello %d"sv, (int)2); + } + { static_assert(false == std::is_signed_v<const float*>); static_assert(false == std::is_unsigned_v<const float*>); - - static_assert(true == std::is_signed_v<float>); + + static_assert(true == std::is_signed_v<float>); static_assert(false == std::is_unsigned_v<float>); } { @@ -250,13 +312,13 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { } { // we shall ignore signedness like snprintf - static_assert(true == jau::cfmt::check(" int -> int %d", (int)1)); + static_assert(true == jau::cfmt::check(" int -> int %d", 1)); static_assert(true == jau::cfmt::check("unsigned int -> int %d", (unsigned int)1)); static_assert(true == jau::cfmt::check("unsigned int -> unsigned int %u", (unsigned int)1)); - static_assert(true == jau::cfmt::check(" int -> unsigned int %u", (int)1)); + static_assert(true == jau::cfmt::check(" int -> unsigned int %u", 1)); static_assert(true == jau::cfmt::check(" char -> int %d", (char)1)); // integral target type > given type static_assert(false == jau::cfmt::check(" error long -> int %d", (long)1)); // error: integral target type < given type - + static_assert(true == jau::cfmt::check(" %d", i)); static_assert(true == jau::cfmt::check(" %f", fa)); static_assert(true == jau::cfmt::check(" %zd", (ssize_t)1)); @@ -264,14 +326,14 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { static_assert(true == jau::cfmt::check(" %" PRIi64 ".", (int64_t)1)); static_assert(true == jau::cfmt::check(" %" PRIi64 ".", sz2)); static_assert(true == jau::cfmt::check(" %p", pf)); - + static_assert(0 == jau::cfmt::checkR("Hello World").argCount()); static_assert(1 == jau::cfmt::checkR("Hello World %d", 1).argCount()); static_assert(1 == jau::cfmt::checkR("Hello 1 %d", i).argCount()); static_assert(true == jau::cfmt::check("Hello World")); static_assert(true == jau::cfmt::check("Hello World %d", 1)); static_assert(true == jau::cfmt::check("Hello 1 %d", i)); - + static_assert(1 == jau::cfmt::checkR("Hello 1 %.2f", fa).argCount()); static_assert(1 == jau::cfmt::checkR("Hello 1 %.2f - end", fa).argCount()); static_assert(2 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f - end", fa, fb).argCount()); @@ -290,8 +352,8 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { static_assert(false == jau::cfmt::check("Hello 1 %.2f, 2 %2.2f, 3 %zu, 4 %" PRIi64 ", 5 %03d, 6 %p - end", fa, fb, sz1, sz2, i, i)); - const std::string s = format_string_static_assert("format_020a: %f, %f, %zu, %" PRIu64 ", %d\n", - fa + 1.0_f32, fb + 1.0_f32, sz1 + 1, sz2 + 1_u64, i + 1); + const std::string s = jau_format_string_static("format_020a: %f, %f, %zu, %" PRIu64 ", %d\n", + fa + 1.0_f32, fb + 1.0_f32, sz1 + 1, sz2 + 1_u64, i + 1); (void)s; } { @@ -299,15 +361,21 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { static_assert( b1, "Not Assignable" ); constexpr bool b2 = std::is_nothrow_assignable_v<unsigned int&, long double>; static_assert( b2, "Not Assignable" ); - format_string_static_assert("Hello"); - format_string_static_assert("Hello %d", (unsigned int)2); - format_string_static_assert("Hello %u", (int)2); - + jau_format_string_static("Hello"); + jau_format_string_static("Hello %d", (int)2); + jau_format_string_static("Hello %d", (unsigned int)2); + jau_format_string_static("Hello %u", (unsigned int)2); + jau_format_string_static("Hello %u", (int)2); + + // constexpr std::string_view fmt2 = "Hello %d"sv; + // format_string_static2("Hello %d"sv, (int)2); + // format_string_static2("Hello %d"s, (int)2); + constexpr jau::cfmt::PResult c1 = jau::cfmt::checkR("Hello %u", (unsigned int)1); std::cerr << "XXX: " << __LINE__ << ": " << c1 << std::endl; REQUIRE(false == c1.error()); } - { + { constexpr jau::cfmt::PResult c1 = jau::cfmt::checkR("Hello World"); REQUIRE(false == c1.error()); REQUIRE(0 == c1.argCount()); @@ -320,17 +388,17 @@ TEST_CASE("jau::cfmt_00", "[jau][std::string][jau::cfmt]") { // `Hello 1 %.2f, 2 %2.2f, 3 %zu, 4 %li, 5 %03d, 6 %p - end` REQUIRE(1 == jau::cfmt::checkR("Hello 1 %.2f", fa).argCount()); REQUIRE(1 == jau::cfmt::checkR("Hello 1 %.2f - end", fa).argCount()); - + // 'Hello 1 %.2f, 2 %2.2f - end' jau::cfmt::PResult pc = jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f - end", fa, fb); - std::cerr << "XXX: " << __LINE__ << ": " << pc << std::endl; + std::cerr << "XXX: " << __LINE__ << ": " << pc << std::endl; REQUIRE(2 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f - end", fa, fb).argCount()); - + // `Hello 1 %.2f, 2 %2.2f, 3 %zu - end` pc = jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f, 3 %zu - end", fa, fb, sz1); std::cerr << "XXX: " << __LINE__ << ": " << pc << std::endl; REQUIRE(3 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f, 3 %zu - end", fa, fb, sz1).argCount()); - + REQUIRE(4 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f, 3 %zu, 4 %" PRIi64 " - end", fa, fb, sz1, sz2).argCount()); REQUIRE(5 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f, 3 %zu, 4 %" PRIi64 ", 5 %03d - end", fa, fb, sz1, sz2, i).argCount()); REQUIRE(6 == jau::cfmt::checkR("Hello 1 %.2f, 2 %2.2f, 3 %zu, 4 %" PRIi64 ", 5 %03d, 6 %p - end", fa, fb, sz1, sz2, i, pf).argCount()); @@ -352,7 +420,7 @@ TEST_CASE("jau::cfmt_01", "[jau][std::string][format_string]") { /// Execute with `test_stringfmt --perf_analysis` TEST_CASE("jau::cfmt_10", "[benchmark][jau][std::string][format_string]") { - const size_t loops = catch_auto_run ? 1000 : 1000; + const size_t loops = 1000; // catch_auto_run ? 1000 : 1000; WARN("Benchmark with " + std::to_string(loops) + " loops"); CHECK(true); @@ -387,6 +455,22 @@ TEST_CASE("jau::cfmt_10", "[benchmark][jau][std::string][format_string]") { } return res; }; + BENCHMARK("fmt__check cnstexp2 bench") { + volatile size_t res = 0; + for( size_t i = 0; i < loops; ++i ) { + constexpr float fa = 1.1f, fb = 2.2f; + constexpr size_t sz1 = 1; + constexpr uint64_t sz2 = 2; + constexpr int i1 = 3; + + constexpr jau::cfmt2::PResult pc = jau::cfmt2::checkR("format_check: %.2f, %2.2f, %zu, %" PRIu64 ", %03d\n", + fa, fb, sz1, sz2, i1); + constexpr size_t r = pc.argCount(); + REQUIRE(5 == r); + res = res + r; + } + return res; + }; BENCHMARK("format_000a_vsnprintf bench") { volatile size_t res = 0; for( size_t i = 0; i < loops; ++i ) { @@ -409,8 +493,8 @@ TEST_CASE("jau::cfmt_10", "[benchmark][jau][std::string][format_string]") { constexpr size_t sz1 = 1; constexpr uint64_t a_u64 = 2; constexpr int j = 3; - const std::string s = format_string_static_assert("format_020a: %f, %f, %zu, %" PRIu64 ", %d\n", - fa + 1.0_f32, fb + 1.0_f32, sz1 + 1, a_u64 + 1_u64, j + 1); + const std::string s = jau_format_string_static("format_020a: %f, %f, %zu, %" PRIu64 ", %d\n", + fa + 1.0_f32, fb + 1.0_f32, sz1 + 1, a_u64 + 1_u64, j + 1); res = res + s.length(); } } |