1 #pragma once
2 
3 #include <phosphor-logging/lg2/flags.hpp>
4 #include <phosphor-logging/lg2/header.hpp>
5 #include <phosphor-logging/lg2/level.hpp>
6 #include <phosphor-logging/lg2/logger.hpp>
7 #include <sdbusplus/message/native_types.hpp>
8 
9 #include <concepts>
10 #include <cstddef>
11 #include <filesystem>
12 #include <format>
13 #include <source_location>
14 #include <string_view>
15 #include <tuple>
16 #include <type_traits>
17 
18 namespace lg2::details
19 {
20 
21 /** Concept to determine if an item acts like a string.
22  *
23  *  Something acts like a string if we can construct a string_view from it.
24  *  This covers std::string and C-strings.  But, there is subtlety in that
25  *  nullptr_t can be used to construct a string_view until C++23, so we need
26  *  to exempt out nullptr_t's (otherwise `nullptr` ends up acting like a
27  *  string).
28  */
29 template <typename T>
30 concept string_like_type =
31     (std::constructible_from<std::string_view, T> ||
32      std::same_as<std::filesystem::path, std::decay_t<T>>) &&
33     !std::same_as<std::nullptr_t, T>;
34 
35 /** Concept to determine if an item acts like a pointer.
36  *
37  *  Any pointer, which doesn't act like a string, should be treated as a raw
38  *  pointer.
39  */
40 template <typename T>
41 concept pointer_type =
42     (std::is_pointer_v<T> || std::same_as<std::nullptr_t, T>) &&
43     !string_like_type<T>;
44 
45 /** Concept to determine if an item acts like an unsigned_integral.
46  *
47  *  Except bool because we want bool to be handled special to create nice
48  *  `True` and `False` strings.
49  */
50 template <typename T>
51 concept unsigned_integral_except_bool =
52     !std::same_as<T, bool> && std::unsigned_integral<T>;
53 
54 template <typename T>
55 concept sdbusplus_enum = sdbusplus::message::has_convert_from_string_v<T>;
56 
57 template <typename T>
58 concept exception_type = std::derived_from<std::decay_t<T>, std::exception>;
59 
60 template <typename T>
61 concept sdbusplus_object_path =
62     std::derived_from<std::decay_t<T>, sdbusplus::message::object_path>;
63 
64 template <typename T>
65 concept has_to_string = requires(T&& t) { to_string(t); };
66 
67 template <typename T>
68 concept is_raw_enum =
69     std::is_enum_v<std::decay_t<T>> && !sdbusplus_enum<T> && !has_to_string<T>;
70 
71 /** Concept listing all of the types we know how to convert into a format
72  *  for logging.
73  */
74 template <typename T>
75 concept unsupported_log_convert_types =
76     !(unsigned_integral_except_bool<T> || std::signed_integral<T> ||
77       std::same_as<bool, T> || std::floating_point<T> || string_like_type<T> ||
78       pointer_type<T> || sdbusplus_enum<T> || exception_type<T> ||
79       sdbusplus_object_path<T> || has_to_string<T> || is_raw_enum<T>);
80 
81 /** Any type we do not know how to convert for logging gives a nicer
82  *  static_assert message. */
83 template <log_flags... Fs, unsupported_log_convert_types T>
log_convert(const char *,log_flag<Fs...>,T)84 static auto log_convert(const char*, log_flag<Fs...>, T)
85 {
86     static_assert(!std::is_same_v<T, T>, "Unsupported type for logging value.");
87     // Having this return of an empty tuple reduces the error messages.
88     return std::tuple<>{};
89 }
90 
91 /** Logging conversion for unsigned. */
92 template <log_flags... Fs, unsigned_integral_except_bool V>
log_convert(const char * h,log_flag<Fs...> f,V v)93 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
94 {
95     // Compile-time checks for valid formatting flags.
96     prohibit(f, floating);
97     prohibit(f, signed_val);
98     prohibit(f, str);
99 
100     one_from_set(f, dec | hex | bin);
101     one_from_set(f, field8 | field16 | field32 | field64);
102 
103     // Add 'unsigned' flag, force to uint64_t for variadic passing.
104     return std::make_tuple(h, (f | unsigned_val).value,
105                            static_cast<uint64_t>(v));
106 }
107 
108 /** Logging conversion for signed. */
109 template <log_flags... Fs, std::signed_integral V>
log_convert(const char * h,log_flag<Fs...> f,V v)110 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
111 {
112     // Compile-time checks for valid formatting flags.
113     prohibit(f, floating);
114     prohibit(f, str);
115     prohibit(f, unsigned_val);
116 
117     one_from_set(f, dec | hex | bin);
118     one_from_set(f, field8 | field16 | field32 | field64);
119 
120     // Add 'signed' flag, force to int64_t for variadic passing.
121     return std::make_tuple(h, (f | signed_val).value, static_cast<int64_t>(v));
122 }
123 
124 /** Logging conversion for bool. */
125 template <log_flags... Fs, std::same_as<bool> V>
log_convert(const char * h,log_flag<Fs...> f,V v)126 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
127 {
128     // Compile-time checks for valid formatting flags.
129     prohibit(f, bin);
130     prohibit(f, dec);
131     prohibit(f, field16);
132     prohibit(f, field32);
133     prohibit(f, field64);
134     prohibit(f, field8);
135     prohibit(f, floating);
136     prohibit(f, hex);
137     prohibit(f, signed_val);
138     prohibit(f, unsigned_val);
139 
140     // Cast bools to a "True" or "False" string.
141     return std::make_tuple(h, (f | str).value, v ? "True" : "False");
142 }
143 
144 /** Logging conversion for floating points. */
145 template <log_flags... Fs, std::floating_point V>
log_convert(const char * h,log_flag<Fs...> f,V v)146 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
147 {
148     // Compile-time checks for valid formatting flags.
149     prohibit(f, bin);
150     prohibit(f, dec);
151     prohibit(f, field16);
152     prohibit(f, field32);
153     prohibit(f, field64);
154     prohibit(f, field8);
155     prohibit(f, hex);
156     prohibit(f, signed_val);
157     prohibit(f, str);
158     prohibit(f, unsigned_val);
159 
160     // Add 'floating' flag, force to double for variadic passing.
161     return std::make_tuple(h, (f | floating).value, static_cast<double>(v));
162 }
163 
164 /** Logging conversion for sdbusplus enums. */
165 template <log_flags... Fs, sdbusplus_enum V>
log_convert(const char * h,log_flag<Fs...> f,V v)166 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
167 {
168     // Compile-time checks for valid formatting flags.
169     prohibit(f, bin);
170     prohibit(f, dec);
171     prohibit(f, field16);
172     prohibit(f, field32);
173     prohibit(f, field64);
174     prohibit(f, field8);
175     prohibit(f, floating);
176     prohibit(f, hex);
177     prohibit(f, signed_val);
178     prohibit(f, unsigned_val);
179 
180     return std::make_tuple(h, (f | str).value,
181                            sdbusplus::message::convert_to_string(v));
182 }
183 
184 /** Logging conversion for string-likes. */
185 template <log_flags... Fs, string_like_type V>
log_convert(const char * h,log_flag<Fs...> f,const V & v)186 static auto log_convert(const char* h, log_flag<Fs...> f, const V& v)
187 {
188     // Compile-time checks for valid formatting flags.
189     prohibit(f, bin);
190     prohibit(f, dec);
191     prohibit(f, field16);
192     prohibit(f, field32);
193     prohibit(f, field64);
194     prohibit(f, field8);
195     prohibit(f, floating);
196     prohibit(f, hex);
197     prohibit(f, signed_val);
198     prohibit(f, unsigned_val);
199 
200     // Utiilty to handle conversion to a 'const char*' depending on V:
201     //  - 'const char*' and similar use static cast.
202     //  - 'std::filesystem::path' use c_str() function.
203     //  - 'std::string' and 'std::string_view' use data() function.
204     auto str_data = [](const V& v) {
205         if constexpr (std::is_same_v<const char*, std::decay_t<V>> ||
206                       std::is_same_v<char*, std::decay_t<V>>)
207         {
208             return static_cast<const char*>(v);
209         }
210         else if constexpr (std::is_same_v<std::filesystem::path,
211                                           std::decay_t<V>>)
212         {
213             return v.c_str();
214         }
215         else
216         {
217             return v.data();
218         }
219     };
220 
221     // Add 'str' flag, force to 'const char*' for variadic passing.
222     return std::make_tuple(h, (f | str).value, str_data(v));
223 }
224 
225 /** Logging conversion for pointer-types. */
226 template <log_flags... Fs, pointer_type V>
log_convert(const char * h,log_flag<Fs...> f,V v)227 static auto log_convert(const char* h, log_flag<Fs...> f, V v)
228 {
229     // Compile-time checks for valid formatting flags.
230     prohibit(f, bin);
231     prohibit(f, dec);
232     prohibit(f, field16);
233     prohibit(f, field32);
234     prohibit(f, field64);
235     prohibit(f, field8);
236     prohibit(f, floating);
237     prohibit(f, signed_val);
238     prohibit(f, str);
239 
240     // Cast (void*) to a hex-formatted uint64 using the target's pointer-size
241     // to determine field-width.
242     constexpr static auto new_f =
243         sizeof(void*) == 4 ? field32.value : field64.value;
244 
245     return std::make_tuple(h, new_f | (hex | unsigned_val).value,
246                            reinterpret_cast<uint64_t>(v));
247 }
248 
249 /* Logging conversion for exceptions. */
250 template <log_flags... Fs, exception_type V>
log_convert(const char * h,log_flag<Fs...> f,V && v)251 static auto log_convert(const char* h, log_flag<Fs...> f, V&& v)
252 {
253     // Compile-time checks for valid formatting flags.
254     prohibit(f, bin);
255     prohibit(f, dec);
256     prohibit(f, field16);
257     prohibit(f, field32);
258     prohibit(f, field64);
259     prohibit(f, field8);
260     prohibit(f, floating);
261     prohibit(f, hex);
262     prohibit(f, signed_val);
263     prohibit(f, unsigned_val);
264 
265     // Treat like a string, but get the 'what' from the exception.
266     return std::make_tuple(h, (f | str).value, v.what());
267 }
268 
269 /* Logging conversion for object path. */
270 template <log_flags... Fs, sdbusplus_object_path V>
log_convert(const char * h,log_flag<Fs...> f,V && v)271 static auto log_convert(const char* h, log_flag<Fs...> f, V&& v)
272 {
273     // Compile-time checks for valid formatting flags.
274     prohibit(f, bin);
275     prohibit(f, dec);
276     prohibit(f, field16);
277     prohibit(f, field32);
278     prohibit(f, field64);
279     prohibit(f, field8);
280     prohibit(f, floating);
281     prohibit(f, hex);
282     prohibit(f, signed_val);
283     prohibit(f, unsigned_val);
284 
285     // Treat like a string, but get the 'str' from the object path.
286     return std::make_tuple(h, (f | str).value, v.str);
287 }
288 
289 template <log_flags... Fs, has_to_string V>
log_convert(const char * h,log_flag<Fs...> f,V && v)290 static auto log_convert(const char* h, log_flag<Fs...> f, V&& v)
291 {
292     // Compile-time checks for valid formatting flags.
293     prohibit(f, bin);
294     prohibit(f, dec);
295     prohibit(f, field16);
296     prohibit(f, field32);
297     prohibit(f, field64);
298     prohibit(f, field8);
299     prohibit(f, floating);
300     prohibit(f, hex);
301     prohibit(f, signed_val);
302     prohibit(f, unsigned_val);
303 
304     // Treat like a string, but call to_string.
305     return std::make_tuple(h, (f | str).value, to_string(std::forward<V>(v)));
306 }
307 
308 template <log_flags... Fs, is_raw_enum V>
log_convert(const char * h,log_flag<Fs...> f,V && v)309 static auto log_convert(const char* h, log_flag<Fs...> f, V&& v)
310 {
311     // Compile-time checks for valid formatting flags.
312     prohibit(f, bin);
313     prohibit(f, dec);
314     prohibit(f, field16);
315     prohibit(f, field32);
316     prohibit(f, field64);
317     prohibit(f, field8);
318     prohibit(f, floating);
319     prohibit(f, hex);
320     prohibit(f, signed_val);
321     prohibit(f, unsigned_val);
322 
323     // Treat like a string, but convert using std::format.
324     return std::make_tuple(
325         h, (f | str).value,
326         std::format("Enum({})",
327                     static_cast<std::underlying_type_t<std::decay_t<V>>>(v)));
328 }
329 
330 /** Class to facilitate walking through the arguments of the `lg2::log` function
331  *  and ensuring correct parameter types and conversion operations.
332  */
333 class log_conversion
334 {
335   private:
336     /** Conversion and validation is complete.  Pass along to the final
337      *  do_log variadic function. */
338     template <typename... Ts>
done(level l,const std::source_location & s,const char * m,Ts &&...ts)339     static void done(level l, const std::source_location& s, const char* m,
340                      Ts&&... ts)
341     {
342         do_log(l, s, m, ts..., nullptr);
343     }
344 
345     /** Apply the tuple from the end of 'step' into done.
346      *
347      *  There are some cases where the tuple must hold a `std::string` in
348      *  order to avoid losing data in a temporary (see sdbusplus-enum
349      *  conversion), but the `do_log` function wants a `const char*` as
350      *  the variadic type.  Run the tuple through a lambda to pull out
351      *  the `const char*`'s without losing the temporary held by the tuple.
352      */
apply_done(const auto & args_tuple)353     static void apply_done(const auto& args_tuple)
354     {
355         auto squash_string = [](auto& arg) -> decltype(auto) {
356             if constexpr (std::is_same_v<const std::string&, decltype(arg)>)
357             {
358                 return arg.data();
359             }
360             else
361             {
362                 return arg;
363             }
364         };
365 
366         std::apply([squash_string](
367                        const auto&... args) { done(squash_string(args)...); },
368                    args_tuple);
369     }
370 
371     /** Handle conversion of a { Header, Flags, Value } argument set. */
372     template <typename... Ts, log_flags... Fs, typename V, typename... Ss>
step(std::tuple<Ts...> && ts,const header_str & h,log_flag<Fs...> f,V && v,Ss &&...ss)373     static void step(std::tuple<Ts...>&& ts, const header_str& h,
374                      log_flag<Fs...> f, V&& v, Ss&&... ss)
375     {
376         static_assert(!std::is_same_v<header_str, std::decay_t<V>>,
377                       "Found header_str as value; suggest using std::string to "
378                       "avoid unintended conversion.");
379 
380         // These two if conditions are similar, except that one calls 'done'
381         // since Ss is empty and the other calls the next 'step'.
382 
383         // 1. Call `log_convert` on {h, f, v} for proper conversion.
384         // 2. Append the results of `log_convert` into the already handled
385         //    arguments (in ts).
386         // 3. Call the next step in the chain to handle the remainder Ss.
387 
388         if constexpr (sizeof...(Ss) == 0)
389         {
390             apply_done(
391                 std::tuple_cat(std::move(ts), log_convert(h.data(), f, v)));
392         }
393         else
394         {
395             step(std::tuple_cat(std::move(ts), log_convert(h.data(), f, v)),
396                  ss...);
397         }
398     }
399 
400     /** Handle conversion of a { Header, Value } argument set. */
401     template <typename... Ts, typename V, typename... Ss>
step(std::tuple<Ts...> && ts,const header_str & h,V && v,Ss &&...ss)402     static void step(std::tuple<Ts...>&& ts, const header_str& h, V&& v,
403                      Ss&&... ss)
404     {
405         static_assert(!std::is_same_v<header_str, std::decay_t<V>>,
406                       "Found header_str as value; suggest using std::string to "
407                       "avoid unintended conversion.");
408         // These two if conditions are similar, except that one calls 'done'
409         // since Ss is empty and the other calls the next 'step'.
410 
411         // 1. Call `log_convert` on {h, <empty flags>, v} for proper conversion.
412         // 2. Append the results of `log_convert` into the already handled
413         //    arguments (in ts).
414         // 3. Call the next step in the chain to handle the remainder Ss.
415 
416         if constexpr (sizeof...(Ss) == 0)
417         {
418             apply_done(std::tuple_cat(std::move(ts),
419                                       log_convert(h.data(), log_flag<>{}, v)));
420         }
421         else
422         {
423             step(std::tuple_cat(std::move(ts),
424                                 log_convert(h.data(), log_flag<>{}, v)),
425                  ss...);
426         }
427     }
428 
429     /** Finding a non-string as the first argument of a 2 or 3 argument set
430      *  is an error (missing HEADER field). */
431     template <typename... Ts, not_constexpr_string H, typename... Ss>
step(std::tuple<Ts...> &&,H,Ss &&...)432     static void step(std::tuple<Ts...>&&, H, Ss&&...)
433     {
434         static_assert(std::is_same_v<header_str, H>,
435                       "Found value without expected header field.");
436     }
437 
438     /** Finding a free string at the end is an error (found HEADER but no data).
439      */
440     template <typename... Ts>
step(std::tuple<Ts...> &&,header_str)441     static void step(std::tuple<Ts...>&&, header_str)
442     {
443         static_assert(std::is_same_v<std::tuple<Ts...>, header_str>,
444                       "Found header field without expected data.");
445     }
446 
447   public:
448     /** Start processing a sequence of arguments to `lg2::log` using `step` or
449      * `done`. */
450     template <typename... Ts>
start(level l,const std::source_location & s,const char * msg,Ts &&...ts)451     static void start(level l, const std::source_location& s, const char* msg,
452                       Ts&&... ts)
453     {
454         // If there are no arguments (ie. just a message), then skip processing
455         // and call `done` directly.
456         if constexpr (sizeof...(Ts) == 0)
457         {
458             done(l, s, msg);
459         }
460         // Handle all the Ts by recursively calling 'step'.
461         else
462         {
463             step(std::forward_as_tuple(l, s, msg), ts...);
464         }
465     }
466 };
467 
468 } // namespace lg2::details
469