aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/libutil/charptr-cast.hh140
-rw-r--r--src/libutil/meson.build1
2 files changed, 141 insertions, 0 deletions
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 <concepts> // IWYU pragma: keep
+#include <type_traits>
+
+namespace nix {
+
+namespace charptr_cast_detail {
+
+/** Custom version of std::decay that does not eat CV qualifiers on \c {char * const}. */
+template<typename T>
+struct DecayArrayInternal
+{
+ using type = T;
+};
+
+template <typename T>
+struct DecayArrayInternal<T[]>
+{
+ using type = T *;
+};
+
+template <typename T, std::size_t N>
+struct DecayArrayInternal<T[N]>
+{
+ using type = T *;
+};
+
+template <typename T>
+using DecayArray = DecayArrayInternal<T>::type;
+
+/** Is a character type for the purposes of safe reinterpret_cast. */
+template<typename T>
+concept IsChar = std::same_as<T, char> || std::same_as<T, unsigned char>;
+
+template<typename T>
+concept IsConvertibleToChar = std::same_as<T, char8_t> || std::same_as<T, void> || IsChar<T>;
+
+template<typename T>
+concept IsDecayOrPointer = std::is_pointer_v<T> || std::is_pointer_v<DecayArray<T>>;
+
+template<typename From, typename To>
+concept ValidQualifiers = requires {
+ // Does not discard const
+ requires !std::is_const_v<From> || std::is_const_v<To>;
+ // Don't deal with volatile
+ requires !std::is_volatile_v<From> && !std::is_volatile_v<To>;
+};
+
+template<typename From, typename To>
+concept BaseCase = requires {
+ // Cannot cast away const
+ requires ValidQualifiers<From, To>;
+ // At base case, neither should be pointers
+ requires !std::is_pointer_v<From> && !std::is_pointer_v<To>;
+ // Finally are the types compatible?
+ requires IsConvertibleToChar<std::remove_cv_t<From>>;
+ requires IsChar<std::remove_cv_t<To>>;
+};
+
+static_assert(BaseCase<char, char>);
+static_assert(BaseCase<unsigned char, char>);
+static_assert(BaseCase<char8_t, char>);
+static_assert(!BaseCase<const char8_t, char>);
+static_assert(!BaseCase<const char8_t, unsigned char>);
+static_assert(BaseCase<void, unsigned char>);
+// Not legal to cast to char8_t
+static_assert(!BaseCase<void, char8_t>);
+// No pointers
+static_assert(!BaseCase<void *, char8_t>);
+static_assert(!BaseCase<char *, char *>);
+
+// Required to be written in the old style because recursion in concepts is not
+// allowed. Personally I think the committee hates fun.
+template<typename From, typename To, typename = void>
+struct RecursionHelper : std::false_type
+{};
+
+template<typename From, typename To>
+struct RecursionHelper<From, To, std::enable_if_t<BaseCase<From, To>>> : std::true_type
+{};
+
+template<typename From, typename To>
+struct RecursionHelper<
+ From,
+ To,
+ std::enable_if_t<std::is_pointer_v<From> && std::is_pointer_v<To> && ValidQualifiers<From, To>>>
+ : RecursionHelper<std::remove_pointer_t<From>, std::remove_pointer_t<To>>
+{};
+
+template<typename From, typename To>
+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<DecayArray<From>, To>::value;
+ requires IsDecayOrPointer<From> && std::is_pointer_v<To>;
+};
+
+static_assert(!IsCharCastable<char **, char *>);
+static_assert(IsCharCastable<char *, char *>);
+static_assert(!IsCharCastable<const char *, char *>);
+static_assert(!IsCharCastable<volatile char *, char *>);
+static_assert(!IsCharCastable<char *, volatile char *>);
+static_assert(IsCharCastable<char *, const char *>);
+static_assert(IsCharCastable<char **, const char **>);
+static_assert(IsCharCastable<char **, const char * const *>);
+static_assert(!IsCharCastable<char * const *, const char **>);
+static_assert(!IsCharCastable<char, char>);
+static_assert(IsCharCastable<const char *, const unsigned char *>);
+static_assert(!IsCharCastable<char [64][64], char **>);
+static_assert(IsCharCastable<char [64], char *>);
+}
+
+/** 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<typename To, typename From>
+ requires charptr_cast_detail::IsCharCastable<From, To>
+inline To charptr_cast(From p)
+{
+ return reinterpret_cast<To>(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',