diff --git a/src/libutil/charptr-cast.hh b/src/libutil/charptr-cast.hh new file mode 100644 index 000000000..990f2ec55 --- /dev/null +++ b/src/libutil/charptr-cast.hh @@ -0,0 +1,140 @@ +#pragma once +/** @file Safe casts between character pointer types. */ + +#include // IWYU pragma: keep +#include + +namespace nix { + +namespace charptr_cast_detail { + +/** Custom version of std::decay that does not eat CV qualifiers on \c {char * const}. */ +template +struct DecayArrayInternal +{ + using type = T; +}; + +template +struct DecayArrayInternal +{ + using type = T *; +}; + +template +struct DecayArrayInternal +{ + using type = T *; +}; + +template +using DecayArray = DecayArrayInternal::type; + +/** Is a character type for the purposes of safe reinterpret_cast. */ +template +concept IsChar = std::same_as || std::same_as; + +template +concept IsConvertibleToChar = std::same_as || std::same_as || IsChar; + +template +concept IsDecayOrPointer = std::is_pointer_v || std::is_pointer_v>; + +template +concept ValidQualifiers = requires { + // Does not discard const + requires !std::is_const_v || std::is_const_v; + // Don't deal with volatile + requires !std::is_volatile_v && !std::is_volatile_v; +}; + +template +concept BaseCase = requires { + // Cannot cast away const + requires ValidQualifiers; + // At base case, neither should be pointers + requires !std::is_pointer_v && !std::is_pointer_v; + // Finally are the types compatible? + requires IsConvertibleToChar>; + requires IsChar>; +}; + +static_assert(BaseCase); +static_assert(BaseCase); +static_assert(BaseCase); +static_assert(!BaseCase); +static_assert(!BaseCase); +static_assert(BaseCase); +// Not legal to cast to char8_t +static_assert(!BaseCase); +// No pointers +static_assert(!BaseCase); +static_assert(!BaseCase); + +// Required to be written in the old style because recursion in concepts is not +// allowed. Personally I think the committee hates fun. +template +struct RecursionHelper : std::false_type +{}; + +template +struct RecursionHelper>> : std::true_type +{}; + +template +struct RecursionHelper< + From, + To, + std::enable_if_t && std::is_pointer_v && ValidQualifiers>> + : RecursionHelper, std::remove_pointer_t> +{}; + +template +concept IsCharCastable = requires { + // We only decay arrays in From for safety reasons. There is almost no reason + // to cast *into* an array and such code probably needs closer inspection + // anyway. + requires RecursionHelper, To>::value; + requires IsDecayOrPointer && std::is_pointer_v; +}; + +static_assert(!IsCharCastable); +static_assert(IsCharCastable); +static_assert(!IsCharCastable); +static_assert(!IsCharCastable); +static_assert(!IsCharCastable); +static_assert(IsCharCastable); +static_assert(IsCharCastable); +static_assert(IsCharCastable); +static_assert(!IsCharCastable); +static_assert(!IsCharCastable); +static_assert(IsCharCastable); +static_assert(!IsCharCastable); +static_assert(IsCharCastable); +} + +/** Casts between character pointers with guaranteed safety. If this compiles, + * it is at least a sound conversion per C++23 ยง7.2.1 line 11. + * + * This will not let you: + * - Turn things into void * + * - Turn things that are not char into char + * - Turn things into things that are not char + * - Cast away const + * + * At every level in the pointer indirections, \c To must as const or more + * const than \c From. + * + * \c From may be any character pointer or void pointer or an array of characters. + * + * N.B. Be careful, the template args are in the possibly-surprising + * order To, From due to deduction. + */ +template + requires charptr_cast_detail::IsCharCastable +inline To charptr_cast(From p) +{ + return reinterpret_cast(p); +} + +} diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 01fe65207..4740ea64d 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -52,6 +52,7 @@ libutil_headers = files( 'box_ptr.hh', 'canon-path.hh', 'cgroup.hh', + 'charptr-cast.hh', 'checked-arithmetic.hh', 'chunked-vector.hh', 'closure.hh',