…I guess that could work too.
0diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp
1index c6ee1ffed9..2f6fad50b7 100644
2--- a/src/test/util_string_tests.cpp
3+++ b/src/test/util_string_tests.cpp
4@@ -7,74 +7,172 @@
5 #include <boost/test/unit_test.hpp>
6
7 using namespace util;
8+using util::detail::CountFormatSpecifiers;
9+
10+namespace {
11+bool CheckTooMany(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Too many conversion specifiers in format string"}; }
12+bool CheckOutOfRange(const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Positional argument out of range"}; }
13+}
14
15 BOOST_AUTO_TEST_SUITE(util_string_tests)
16
17-BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec)
18+// These are counted correctly, since tinyformat will require the provided number of args.
19+BOOST_AUTO_TEST_CASE(ConstevalFormatString_CorrectCounts)
20 {
21- // Compile-time sanity checks
22- static_assert([] {
23- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("");
24- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%");
25- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s");
26- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%%s");
27- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("s%%");
28- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%%%s");
29- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%%");
30- ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers(" 1$s");
31- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s");
32- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$s%1$s");
33- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s");
34- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$s 4$s %2$s");
35- ConstevalFormatString<129>::Detail_CheckNumFormatSpecifiers("%129$s 999$s %2$s");
36- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%02d");
37- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
38- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%+2s");
39- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.6i");
40- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%5.2f");
41- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%#x");
42- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$5i");
43- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$-5i");
44- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$.5i");
45- // tinyformat accepts almost any "type" spec, even '%', or '_', or '\n'.
46- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%");
47- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%123%s");
48- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%_");
49- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%\n");
50- return true; // All checks above compiled and passed
51- }());
52- static_assert([] {
53+ static_assert(CountFormatSpecifiers("") == 0);
54+ tfm::format("");
55+
56+ static_assert(CountFormatSpecifiers("%%") == 0);
57+ tfm::format("%%");
58+
59+ static_assert(CountFormatSpecifiers("%s") == 1);
60+ BOOST_CHECK_EXCEPTION(tfm::format("%s"), tfm::format_error, CheckTooMany);
61+ tfm::format("%s", "foo");
62+
63+ static_assert(CountFormatSpecifiers("%%s") == 0);
64+ tfm::format("%%s");
65+
66+ static_assert(CountFormatSpecifiers("s%%") == 0);
67+ tfm::format("s%%");
68+
69+ static_assert(CountFormatSpecifiers("%%%s") == 1);
70+ BOOST_CHECK_EXCEPTION(tfm::format("%%%s"), tfm::format_error, CheckTooMany);
71+ tfm::format("%%%s", "foo");
72+
73+ static_assert(CountFormatSpecifiers("%s%%") == 1);
74+ BOOST_CHECK_EXCEPTION(tfm::format("%s%%"), tfm::format_error, CheckTooMany);
75+ tfm::format("%s%%", "foo");
76+
77+ static_assert(CountFormatSpecifiers(" 1$s") == 0);
78+ tfm::format(" 1$s");
79+
80+ static_assert(CountFormatSpecifiers("%1$s") == 1);
81+ BOOST_CHECK_EXCEPTION(tfm::format("%1$s"), tfm::format_error, CheckOutOfRange);
82+ tfm::format("%1$s", "foo");
83+
84+ static_assert(CountFormatSpecifiers("%1$s%1$s") == 1);
85+ BOOST_CHECK_EXCEPTION(tfm::format("%1$s%1$s"), tfm::format_error, CheckOutOfRange);
86+ tfm::format("%1$s%1$s", "foo");
87+
88+ static_assert(CountFormatSpecifiers("%2$s") == 2);
89+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s"), tfm::format_error, CheckOutOfRange);
90+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s", "foo"), tfm::format_error, CheckOutOfRange);
91+ tfm::format("%2$s", "foo", "bar");
92+
93+ static_assert(CountFormatSpecifiers("%2$s 4$s %2$s") == 2);
94+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s"), tfm::format_error, CheckOutOfRange);
95+ BOOST_CHECK_EXCEPTION(tfm::format("%2$s 4$s %2$s", "foo"), tfm::format_error, CheckOutOfRange);
96+ tfm::format("%2$s 4$s %2$s", "foo", "bar");
97+
98+ static_assert(CountFormatSpecifiers("%12$s 99$s %2$s") == 12);
99+ BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s"), tfm::format_error, CheckOutOfRange);
100+ BOOST_CHECK_EXCEPTION(tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"), tfm::format_error, CheckOutOfRange);
101+ tfm::format("%12$s 99$s %2$s", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12");
102+
103+ static_assert(CountFormatSpecifiers("%02d") == 1);
104+ BOOST_CHECK_EXCEPTION(tfm::format("%02d"), tfm::format_error, CheckTooMany);
105+ tfm::format("%02d", 1);
106+
107+ static_assert(CountFormatSpecifiers("%+2s") == 1);
108+ BOOST_CHECK_EXCEPTION(tfm::format("%+2s"), tfm::format_error, CheckTooMany);
109+ tfm::format("%+2s", 1);
110+
111+ static_assert(CountFormatSpecifiers("%.6i") == 1);
112+ BOOST_CHECK_EXCEPTION(tfm::format("%.6i"), tfm::format_error, CheckTooMany);
113+ tfm::format("%.6i", 1);
114+
115+ static_assert(CountFormatSpecifiers("%5.2f") == 1);
116+ BOOST_CHECK_EXCEPTION(tfm::format("%5.2f"), tfm::format_error, CheckTooMany);
117+ tfm::format("%5.2f", 1);
118+
119+ static_assert(CountFormatSpecifiers("%1$5i") == 1);
120+ BOOST_CHECK_EXCEPTION(tfm::format("%1$5i"), tfm::format_error, CheckOutOfRange);
121+ tfm::format("%1$5i", 1);
122+
123+ static_assert(CountFormatSpecifiers("%1$-5i") == 1);
124+ BOOST_CHECK_EXCEPTION(tfm::format("%1$-5i"), tfm::format_error, CheckOutOfRange);
125+ tfm::format("%1$-5i", 1);
126+
127+ static_assert(CountFormatSpecifiers("%1$.5i") == 1);
128+ BOOST_CHECK_EXCEPTION(tfm::format("%1$.5i"), tfm::format_error, CheckOutOfRange);
129+ tfm::format("%1$.5i", 1);
130+
131+ static_assert(CountFormatSpecifiers("%#x") == 1);
132+ BOOST_CHECK_EXCEPTION(tfm::format("%#x"), tfm::format_error, CheckTooMany);
133+ tfm::format("%#x", 1);
134+
135+ static_assert(CountFormatSpecifiers("%123%") == 1);
136+ BOOST_CHECK_EXCEPTION(tfm::format("%123%"), tfm::format_error, CheckTooMany);
137+ tfm::format("%123%", 1);
138+
139+ static_assert(CountFormatSpecifiers("%123%s") == 1);
140+ BOOST_CHECK_EXCEPTION(tfm::format("%123%s"), tfm::format_error, CheckTooMany);
141+ tfm::format("%123%s", 1);
142+
143+ static_assert(CountFormatSpecifiers("%_") == 1);
144+ BOOST_CHECK_EXCEPTION(tfm::format("%_"), tfm::format_error, CheckTooMany);
145+ tfm::format("%_", 1);
146+
147+ static_assert(CountFormatSpecifiers("%\n") == 1);
148+ BOOST_CHECK_EXCEPTION(tfm::format("%\n"), tfm::format_error, CheckTooMany);
149+ tfm::format("%\n", 1);
150+}
151+
152 // The `*` specifier behavior is unsupported and can lead to runtime
153 // errors when used in a ConstevalFormatString. Please refer to the
154 // note in the ConstevalFormatString docs.
155- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%*c");
156- ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%2$*3$d");
157- ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%.*f");
158- return true; // All checks above compiled and passed
159- }());
160+BOOST_AUTO_TEST_CASE(ConstevalFormatString_IncorrectCounts)
161+{
162+ int a{}, b{}, c{};
163
164- // Negative checks at runtime
165+ auto check_not_enough{[](const tfm::format_error& s) { return s.what() == std::string_view{"tinyformat: Not enough arguments to read variable width or precision"}; }};
166+ static_assert(CountFormatSpecifiers("%*c") == 1);
167+ BOOST_CHECK_EXCEPTION(tfm::format("%*c"), tfm::format_error, check_not_enough);
168+ BOOST_CHECK_EXCEPTION(tfm::format("%*c", a), tfm::format_error, CheckTooMany);
169+ tfm::format("%*c", a, b);
170+
171+ static_assert(CountFormatSpecifiers("%2$*3$d") == 2);
172+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d"), tfm::format_error, CheckOutOfRange);
173+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a), tfm::format_error, CheckOutOfRange);
174+ BOOST_CHECK_EXCEPTION(tfm::format("%2$*3$d", a, b), tfm::format_error, CheckOutOfRange);
175+ tfm::format("%2$*3$d", a, b, c);
176+
177+ static_assert(CountFormatSpecifiers("%.*f") == 1);
178+ BOOST_CHECK_EXCEPTION(tfm::format("%.*f"), tfm::format_error, check_not_enough);
179+ BOOST_CHECK_EXCEPTION(tfm::format("%.*f", a), tfm::format_error, CheckTooMany);
180+ tfm::format("%.*f", a, b);
181+}
182+
183+// Negative checks. Executed at runtime as exceptions are not allowed at compile time.
184+BOOST_AUTO_TEST_CASE(ConstevalFormatString_NegativeChecks)
185+{
186 using ErrType = const char*;
187
188 auto check_mix{[](const ErrType& str) { return str == std::string_view{"Format specifiers must be all positional or all non-positional!"}; }};
189- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%s%1$s"), ErrType, check_mix);
190-
191- auto check_num_spec{[](const ErrType& str) { return str == std::string_view{"Format specifier count must match the argument count!"}; }};
192- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers(""), ErrType, check_num_spec);
193- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
194- BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%s"), ErrType, check_num_spec);
195- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
196- BOOST_CHECK_EXCEPTION(ConstevalFormatString<2>::Detail_CheckNumFormatSpecifiers("%1$s"), ErrType, check_num_spec);
197+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%s%1$s"), ErrType, check_mix);
198
199 auto check_0_pos{[](const ErrType& str) { return str == std::string_view{"Positional format specifier must have position of at least 1"}; }};
200- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$s"), ErrType, check_0_pos);
201- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%$"), ErrType, check_0_pos);
202- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$"), ErrType, check_0_pos);
203- BOOST_CHECK_EXCEPTION(ConstevalFormatString<0>::Detail_CheckNumFormatSpecifiers("%0$s"), ErrType, check_0_pos);
204+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$s"), ErrType, check_0_pos);
205+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%$"), ErrType, check_0_pos);
206+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$"), ErrType, check_0_pos);
207+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%0$s"), ErrType, check_0_pos);
208
209 auto check_term{[](const ErrType& str) { return str == std::string_view{"Format specifier incorrectly terminated by end of string"}; }};
210- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%"), ErrType, check_term);
211- BOOST_CHECK_EXCEPTION(ConstevalFormatString<1>::Detail_CheckNumFormatSpecifiers("%1$"), ErrType, check_term);
212+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%"), ErrType, check_term);
213+ BOOST_CHECK_EXCEPTION(CountFormatSpecifiers("%1$"), ErrType, check_term);
214+}
215+
216+// Existing "tests" in run-lint-format-strings.py parse_string_content
217+BOOST_AUTO_TEST_CASE(ConstevalFormatString_run_lint_format_strings_parse_string_content)
218+{
219+ static_assert(CountFormatSpecifiers("foo bar foo") == 0);
220+ static_assert(CountFormatSpecifiers("foo %d bar foo") == 1);
221+ static_assert(CountFormatSpecifiers("foo %d bar %i foo") == 2);
222+ static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo") == 2);
223+ static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %d foo") == 3);
224+ //static_assert(CountFormatSpecifiers("foo %d bar %i foo %% foo %*d foo") == 4); // not implemented
225+ static_assert(CountFormatSpecifiers("foo %5$d") == 5);
226+ //static_assert(CountFormatSpecifiers("foo %5$*7$d") == 7); // not implemented
227 }
228
229 BOOST_AUTO_TEST_SUITE_END()
230diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp
231index 1624fb8b5b..bd40d596ef 100644
232--- a/src/test/util_tests.cpp
233+++ b/src/test/util_tests.cpp
234@@ -150,22 +150,24 @@ constexpr uint8_t HEX_PARSE_OUTPUT[] = {
235 static_assert((sizeof(HEX_PARSE_INPUT) - 1) == 2 * sizeof(HEX_PARSE_OUTPUT));
236 BOOST_AUTO_TEST_CASE(parse_hex)
237 {
238+ using util::hex_literals::detail::Hex;
239+
240 std::vector<unsigned char> result;
241
242 // Basic test vector
243 std::vector<unsigned char> expected(std::begin(HEX_PARSE_OUTPUT), std::end(HEX_PARSE_OUTPUT));
244- constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<util::detail::Hex(HEX_PARSE_INPUT)>()};
245+ constexpr std::array<std::byte, 65> hex_literal_array{operator""_hex<Hex(HEX_PARSE_INPUT)>()};
246 auto hex_literal_span{MakeUCharSpan(hex_literal_array)};
247 BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
248
249- const std::vector<std::byte> hex_literal_vector{operator""_hex_v<util::detail::Hex(HEX_PARSE_INPUT)>()};
250+ const std::vector<std::byte> hex_literal_vector{operator""_hex_v<Hex(HEX_PARSE_INPUT)>()};
251 hex_literal_span = MakeUCharSpan(hex_literal_vector);
252 BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_span.begin(), hex_literal_span.end(), expected.begin(), expected.end());
253
254- constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<util::detail::Hex(HEX_PARSE_INPUT)>()};
255+ constexpr std::array<uint8_t, 65> hex_literal_array_uint8{operator""_hex_u8<Hex(HEX_PARSE_INPUT)>()};
256 BOOST_CHECK_EQUAL_COLLECTIONS(hex_literal_array_uint8.begin(), hex_literal_array_uint8.end(), expected.begin(), expected.end());
257
258- result = operator""_hex_v_u8<util::detail::Hex(HEX_PARSE_INPUT)>();
259+ result = operator""_hex_v_u8<Hex(HEX_PARSE_INPUT)>();
260 BOOST_CHECK_EQUAL_COLLECTIONS(result.begin(), result.end(), expected.begin(), expected.end());
261
262 result = ParseHex(HEX_PARSE_INPUT);
263diff --git a/src/util/strencodings.h b/src/util/strencodings.h
264index 1543de03ab..b79806c251 100644
265--- a/src/util/strencodings.h
266+++ b/src/util/strencodings.h
267@@ -427,16 +427,16 @@ struct Hex {
268
269 } // namespace detail
270
271-template <util::detail::Hex str>
272+template <util::hex_literals::detail::Hex str>
273 constexpr auto operator""_hex() { return str.bytes; }
274
275-template <util::detail::Hex str>
276+template <util::hex_literals::detail::Hex str>
277 constexpr auto operator""_hex_u8() { return std::bit_cast<std::array<uint8_t, str.bytes.size()>>(str.bytes); }
278
279-template <util::detail::Hex str>
280+template <util::hex_literals::detail::Hex str>
281 constexpr auto operator""_hex_v() { return std::vector<std::byte>{str.bytes.begin(), str.bytes.end()}; }
282
283-template <util::detail::Hex str>
284+template <util::hex_literals::detail::Hex str>
285 inline auto operator""_hex_v_u8() { return std::vector<uint8_t>{UCharCast(str.bytes.data()), UCharCast(str.bytes.data() + str.bytes.size())}; }
286
287 } // inline namespace hex_literals
288diff --git a/src/util/string.h b/src/util/string.h
289index 4724d881ff..d99c1963c6 100644
290--- a/src/util/string.h
291+++ b/src/util/string.h
292@@ -18,25 +18,9 @@
293 #include <vector>
294
295 namespace util {
296-/**
297- * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
298- *
299- * This struct can be used to enforce partial compile-time validation of format
300- * strings, to reduce the likelihood of tinyformat throwing exceptions at
301- * run-time. Validation is partial to try and prevent the most common errors
302- * while avoiding re-implementing the entire parsing logic.
303- *
304- * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
305- * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
306- * they are not used in the codebase. Usage of these fields is not counted and
307- * can lead to run-time exceptions. Code wanting to use the `*` specifier can
308- * side-step this struct and call tinyformat directly.
309- */
310-template <unsigned num_params>
311-struct ConstevalFormatString {
312- const char* const fmt;
313- consteval ConstevalFormatString(const char* str) : fmt{str} { Detail_CheckNumFormatSpecifiers(fmt); }
314- constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
315+
316+namespace detail {
317+constexpr static unsigned CountFormatSpecifiers(std::string_view str)
318 {
319 unsigned count_normal{0}; // Number of "normal" specifiers, like %s
320 unsigned count_pos{0}; // Max number in positional specifier, like %8$s
321@@ -74,8 +58,31 @@ struct ConstevalFormatString {
322 // specifier is not checked. Parsing continues with the next '%'.
323 }
324 if (count_normal && count_pos) throw "Format specifiers must be all positional or all non-positional!";
325- unsigned count{count_normal | count_pos};
326- if (num_params != count) throw "Format specifier count must match the argument count!";
327+ return count_normal | count_pos;
328+}
329+} // namespace detail
330+
331+/**
332+ * [@brief](/bitcoin-bitcoin/contributor/brief/) A wrapper for a compile-time partially validated format string
333+ *
334+ * This struct can be used to enforce partial compile-time validation of format
335+ * strings, to reduce the likelihood of tinyformat throwing exceptions at
336+ * run-time. Validation is partial to try and prevent the most common errors
337+ * while avoiding re-implementing the entire parsing logic.
338+ *
339+ * [@note](/bitcoin-bitcoin/contributor/note/) Counting of `*` dynamic width and precision fields (such as `%*c`,
340+ * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
341+ * they are not used in the codebase. Usage of these fields is not counted and
342+ * can lead to run-time exceptions. Code wanting to use the `*` specifier can
343+ * side-step this struct and call tinyformat directly.
344+ */
345+template <unsigned num_params>
346+struct ConstevalFormatString {
347+ const char* const fmt;
348+ consteval ConstevalFormatString(const char* str) : fmt{str}
349+ {
350+ if (detail::CountFormatSpecifiers(fmt) != num_params)
351+ throw "Format specifier count must match the argument count!";
352 }
353 };