Serenity Operating System
1/*
2 * Copyright (c) 2021, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#pragma once
8
9#include <AK/AllOf.h>
10#include <AK/AnyOf.h>
11#include <AK/Array.h>
12#include <AK/StringView.h>
13
14#ifdef ENABLE_COMPILETIME_FORMAT_CHECK
15// FIXME: Seems like clang doesn't like calling 'consteval' functions inside 'consteval' functions quite the same way as GCC does,
16// it seems to entirely forget that it accepted that parameters to a 'consteval' function to begin with.
17# if defined(AK_COMPILER_CLANG) || defined(__CLION_IDE__) || defined(__CLION_IDE_)
18# undef ENABLE_COMPILETIME_FORMAT_CHECK
19# endif
20#endif
21
22#ifdef ENABLE_COMPILETIME_FORMAT_CHECK
23namespace AK::Format::Detail {
24
25// We have to define a local "purely constexpr" Array that doesn't lead back to us (via e.g. VERIFY)
26template<typename T, size_t Size>
27struct Array {
28 constexpr static size_t size() { return Size; }
29 constexpr T const& operator[](size_t index) const { return __data[index]; }
30 constexpr T& operator[](size_t index) { return __data[index]; }
31 using ConstIterator = SimpleIterator<const Array, T const>;
32 using Iterator = SimpleIterator<Array, T>;
33
34 constexpr ConstIterator begin() const { return ConstIterator::begin(*this); }
35 constexpr Iterator begin() { return Iterator::begin(*this); }
36
37 constexpr ConstIterator end() const { return ConstIterator::end(*this); }
38 constexpr Iterator end() { return Iterator::end(*this); }
39
40 T __data[Size];
41};
42
43template<size_t N>
44consteval auto extract_used_argument_index(char const (&fmt)[N], size_t specifier_start_index, size_t specifier_end_index, size_t& next_implicit_argument_index)
45{
46 struct {
47 size_t index_value { 0 };
48 bool saw_explicit_index { false };
49 } state;
50 for (size_t i = specifier_start_index; i < specifier_end_index; ++i) {
51 auto c = fmt[i];
52 if (c > '9' || c < '0')
53 break;
54
55 state.index_value *= 10;
56 state.index_value += c - '0';
57 state.saw_explicit_index = true;
58 }
59
60 if (!state.saw_explicit_index)
61 return next_implicit_argument_index++;
62
63 return state.index_value;
64}
65
66// FIXME: We should rather parse these format strings at compile-time if possible.
67template<size_t N>
68consteval auto count_fmt_params(char const (&fmt)[N])
69{
70 struct {
71 // FIXME: Switch to variable-sized storage whenever we can come up with one :)
72 Array<size_t, 128> used_arguments { 0 };
73 size_t total_used_argument_count { 0 };
74 size_t next_implicit_argument_index { 0 };
75 bool has_explicit_argument_references { false };
76
77 size_t unclosed_braces { 0 };
78 size_t extra_closed_braces { 0 };
79 size_t nesting_level { 0 };
80
81 Array<size_t, 4> last_format_specifier_start { 0 };
82 size_t total_used_last_format_specifier_start_count { 0 };
83 } result;
84
85 for (size_t i = 0; i < N; ++i) {
86 auto ch = fmt[i];
87 switch (ch) {
88 case '{':
89 if (i + 1 < N && fmt[i + 1] == '{') {
90 ++i;
91 continue;
92 }
93
94 // Note: There's no compile-time throw, so we have to abuse a compile-time string to store errors.
95 if (result.total_used_last_format_specifier_start_count >= result.last_format_specifier_start.size() - 1)
96 compiletime_fail("Format-String Checker internal error: Format specifier nested too deep");
97
98 result.last_format_specifier_start[result.total_used_last_format_specifier_start_count++] = i + 1;
99
100 ++result.unclosed_braces;
101 ++result.nesting_level;
102 break;
103 case '}':
104 if (result.nesting_level == 0) {
105 if (i + 1 < N && fmt[i + 1] == '}') {
106 ++i;
107 continue;
108 }
109 }
110 if (result.unclosed_braces) {
111 --result.nesting_level;
112 --result.unclosed_braces;
113
114 if (result.total_used_last_format_specifier_start_count == 0)
115 compiletime_fail("Format-String Checker internal error: Expected location information");
116
117 auto const specifier_start_index = result.last_format_specifier_start[--result.total_used_last_format_specifier_start_count];
118
119 if (result.total_used_argument_count >= result.used_arguments.size())
120 compiletime_fail("Format-String Checker internal error: Too many format arguments in format string");
121
122 auto used_argument_index = extract_used_argument_index<N>(fmt, specifier_start_index, i, result.next_implicit_argument_index);
123 if (used_argument_index + 1 != result.next_implicit_argument_index)
124 result.has_explicit_argument_references = true;
125 result.used_arguments[result.total_used_argument_count++] = used_argument_index;
126
127 } else {
128 ++result.extra_closed_braces;
129 }
130 break;
131 default:
132 continue;
133 }
134 }
135 return result;
136}
137}
138
139#endif
140
141namespace AK::Format::Detail {
142template<typename... Args>
143struct CheckedFormatString {
144 template<size_t N>
145 consteval CheckedFormatString(char const (&fmt)[N])
146 : m_string { fmt, N - 1 }
147 {
148#ifdef ENABLE_COMPILETIME_FORMAT_CHECK
149 check_format_parameter_consistency<N, sizeof...(Args)>(fmt);
150#endif
151 }
152
153 template<typename T>
154 CheckedFormatString(T const& unchecked_fmt)
155 requires(requires(T t) { StringView { t }; })
156 : m_string(unchecked_fmt)
157 {
158 }
159
160 auto view() const { return m_string; }
161
162private:
163#ifdef ENABLE_COMPILETIME_FORMAT_CHECK
164 template<size_t N, size_t param_count>
165 consteval static bool check_format_parameter_consistency(char const (&fmt)[N])
166 {
167 auto check = count_fmt_params<N>(fmt);
168 if (check.unclosed_braces != 0)
169 compiletime_fail("Extra unclosed braces in format string");
170 if (check.extra_closed_braces != 0)
171 compiletime_fail("Extra closing braces in format string");
172
173 {
174 auto begin = check.used_arguments.begin();
175 auto end = check.used_arguments.begin() + check.total_used_argument_count;
176 auto has_all_referenced_arguments = !AK::any_of(begin, end, [](auto& entry) { return entry >= param_count; });
177 if (!has_all_referenced_arguments)
178 compiletime_fail("Format string references nonexistent parameter");
179 }
180
181 if (!check.has_explicit_argument_references && check.total_used_argument_count != param_count)
182 compiletime_fail("Format string does not reference all passed parameters");
183
184 // Ensure that no passed parameter is ignored or otherwise not referenced in the format
185 // As this check is generally pretty expensive, try to avoid it where it cannot fail.
186 // We will only do this check if the format string has explicit argument refs
187 // otherwise, the check above covers this check too, as implicit refs
188 // monotonically increase, and cannot have 'gaps'.
189 if (check.has_explicit_argument_references) {
190 auto all_parameters = iota_array<size_t, param_count>(0);
191 constexpr auto contains = [](auto begin, auto end, auto entry) {
192 for (; begin != end; begin++) {
193 if (*begin == entry)
194 return true;
195 }
196
197 return false;
198 };
199 auto references_all_arguments = AK::all_of(
200 all_parameters,
201 [&](auto& entry) {
202 return contains(
203 check.used_arguments.begin(),
204 check.used_arguments.begin() + check.total_used_argument_count,
205 entry);
206 });
207 if (!references_all_arguments)
208 compiletime_fail("Format string does not reference all passed parameters");
209 }
210
211 return true;
212 }
213#endif
214
215 StringView m_string;
216};
217}
218
219namespace AK {
220
221template<typename... Args>
222using CheckedFormatString = Format::Detail::CheckedFormatString<IdentityType<Args>...>;
223
224}