From c3929c78f37131c61fb0b3d4c6da57ac3db69f34 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Sun, 23 Mar 2025 15:36:11 +0100 Subject: [PATCH] libutil: disallow enum serialization by default allow opting in to serialization as integers via a trait type instead, and add string-list serializers for the feature flag set enumerations. fixes #738 Change-Id: I2746eb5ef1f15c01b4e681f9ba1615b6c6e64f44 --- doc/manual/rl-next/feature-enum-json.md | 9 +++++++++ lix/libstore/store-api.hh | 4 ++++ lix/libutil/deprecated-features.cc | 16 ++++++++++++++++ lix/libutil/deprecated-features.hh | 8 ++++++++ lix/libutil/error.hh | 5 +++++ lix/libutil/experimental-features.cc | 16 ++++++++++++++++ lix/libutil/experimental-features.hh | 8 ++++++++ lix/libutil/json-fwd.hh | 7 ++++++- lix/libutil/json.hh | 18 +++++++++++++++++- lix/libutil/logging.hh | 6 ++++++ meson.build | 3 +++ tests/unit/libutil/config.cc | 21 +++++++++++++++++++++ 12 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 doc/manual/rl-next/feature-enum-json.md diff --git a/doc/manual/rl-next/feature-enum-json.md b/doc/manual/rl-next/feature-enum-json.md new file mode 100644 index 000000000..97af3738f --- /dev/null +++ b/doc/manual/rl-next/feature-enum-json.md @@ -0,0 +1,9 @@ +--- +synopsis: "Fix experimental and deprecated features showing as integers in `nix config show --json`" +issues: [fj#738] +cls: [2882] +category: "Fixes" +credits: ["horrors"] +--- + +Internal changes in 2.92 caused `nix config show --json` to show deprecated and experimental features not as the list of named features 2.91 and earlier produced, but as integers. This has been fixed. diff --git a/lix/libstore/store-api.hh b/lix/libstore/store-api.hh index 36f8c2281..9cd13f8b3 100644 --- a/lix/libstore/store-api.hh +++ b/lix/libstore/store-api.hh @@ -4,6 +4,7 @@ #include "lix/libutil/archive.hh" #include "lix/libutil/async-io.hh" #include "lix/libutil/async.hh" +#include "lix/libutil/json-fwd.hh" #include "lix/libutil/logging.hh" #include "lix/libstore/nar-info.hh" #include "lix/libstore/realisation.hh" @@ -108,6 +109,9 @@ BuildMode buildModeFromInteger(int); enum TrustedFlag : bool { NotTrusted = false, Trusted = true }; +template<> +struct json::is_integral_enum : std::true_type {}; + struct BuildResult; struct KeyedBuildResult; diff --git a/lix/libutil/deprecated-features.cc b/lix/libutil/deprecated-features.cc index f4737158e..c290e9359 100644 --- a/lix/libutil/deprecated-features.cc +++ b/lix/libutil/deprecated-features.cc @@ -102,4 +102,20 @@ void from_json(const JSON & j, DeprecatedFeature & feature) throw Error("Unknown deprecated feature '%s' in JSON input", input); } +void to_json(JSON & j, const DeprecatedFeatures & f) +{ + StringSet res; + for (auto & depFeature : depFeatureDetails) { + if ((f & depFeature.tag) == (DeprecatedFeatures{} | depFeature.tag)) { + res.emplace(depFeature.name); + } + } + j = res; +} + +void from_json(const JSON & j, DeprecatedFeatures & f) +{ + f = parseDeprecatedFeatures(j.get()); +} + } diff --git a/lix/libutil/deprecated-features.hh b/lix/libutil/deprecated-features.hh index f8c176e20..5cfa386d7 100644 --- a/lix/libutil/deprecated-features.hh +++ b/lix/libutil/deprecated-features.hh @@ -4,6 +4,7 @@ #include "lix/libutil/error.hh" #include "lix/libutil/json-fwd.hh" #include "lix/libutil/types.hh" +#include namespace nix { @@ -19,6 +20,10 @@ enum struct DeprecatedFeature NumDepFeatures, // number of available deprecated features, do not use }; +template<> +struct json::avoids_null : std::true_type +{}; + enum struct DeprecatedFeatures {}; inline DeprecatedFeatures operator| (DeprecatedFeatures a, DeprecatedFeatures b) { @@ -78,6 +83,9 @@ JSON documentDeprecatedFeatures(); void to_json(JSON &, const DeprecatedFeature &); void from_json(const JSON &, DeprecatedFeature &); +void to_json(JSON &, const DeprecatedFeatures &); +void from_json(const JSON &, DeprecatedFeatures &); + /** * A deprecated feature used for some * operation, but was not enabled. diff --git a/lix/libutil/error.hh b/lix/libutil/error.hh index a81f3f9cd..bd838f635 100644 --- a/lix/libutil/error.hh +++ b/lix/libutil/error.hh @@ -15,6 +15,7 @@ * See libutil/tests/logging.cc for usage examples. */ +#include "lix/libutil/json-fwd.hh" #include "lix/libutil/suggestions.hh" #include "lix/libutil/fmt.hh" @@ -27,6 +28,7 @@ #include #include #include +#include namespace nix { @@ -42,6 +44,9 @@ typedef enum { lvlVomit } Verbosity; +template<> +struct json::is_integral_enum : std::true_type {}; + Verbosity verbosityFromIntClamped(int val); /** diff --git a/lix/libutil/experimental-features.cc b/lix/libutil/experimental-features.cc index e8989c509..23567475d 100644 --- a/lix/libutil/experimental-features.cc +++ b/lix/libutil/experimental-features.cc @@ -102,4 +102,20 @@ void from_json(const JSON & j, ExperimentalFeature & feature) throw Error("Unknown experimental feature '%s' in JSON input", input); } +void to_json(JSON & j, const ExperimentalFeatures & f) +{ + StringSet res; + for (auto & xpFeature : xpFeatureDetails) { + if ((f & xpFeature.tag) == (ExperimentalFeatures{} | xpFeature.tag)) { + res.emplace(xpFeature.name); + } + } + j = res; +} + +void from_json(const JSON & j, ExperimentalFeatures & f) +{ + f = parseFeatures(j.get()); +} + } diff --git a/lix/libutil/experimental-features.hh b/lix/libutil/experimental-features.hh index 1026323a4..6f28c2dea 100644 --- a/lix/libutil/experimental-features.hh +++ b/lix/libutil/experimental-features.hh @@ -4,6 +4,7 @@ #include "lix/libutil/error.hh" #include "lix/libutil/json-fwd.hh" #include "lix/libutil/types.hh" +#include namespace nix { @@ -16,6 +17,10 @@ enum struct ExperimentalFeature NumXpFeatures, // number of available experimental features, do not use }; +template<> +struct json::avoids_null : std::true_type +{}; + enum struct ExperimentalFeatures {}; inline ExperimentalFeatures operator| (ExperimentalFeatures a, ExperimentalFeatures b) { @@ -75,6 +80,9 @@ JSON documentExperimentalFeatures(); void to_json(JSON &, const ExperimentalFeature &); void from_json(const JSON &, ExperimentalFeature &); +void to_json(JSON &, const ExperimentalFeatures &); +void from_json(const JSON &, ExperimentalFeatures &); + /** * An experimental feature was required for some (experimental) * operation, but was not enabled. diff --git a/lix/libutil/json-fwd.hh b/lix/libutil/json-fwd.hh index 170bf1f80..fb024384a 100644 --- a/lix/libutil/json-fwd.hh +++ b/lix/libutil/json-fwd.hh @@ -2,6 +2,7 @@ ///@file Lix-specific JSON handling (forward declarations only). #include +#include namespace nix { @@ -16,7 +17,11 @@ template struct avoids_null; template -struct is_integral_enum; +struct is_integral_enum : std::false_type +{}; + +template +concept IntegralEnum = is_integral_enum::value; template struct adl_serializer; diff --git a/lix/libutil/json.hh b/lix/libutil/json.hh index 03e452b38..7f8333532 100644 --- a/lix/libutil/json.hh +++ b/lix/libutil/json.hh @@ -4,6 +4,8 @@ /// overrides on out-of-tree users of libutil, as we'd be required to when /// specializing templates in the nlohmann namespace. nlohmann::json can't /// deal with `std::optional` types until 3.11.3 at, and we need those. +/// We also customize enum serialization to not automatically cast to int; +/// nlohmann can be told to disable this only via a special global define. #include "lix/libutil/json-fwd.hh" // IWYU pragma: keep #include @@ -30,7 +32,7 @@ namespace json { */ template struct avoids_null - : std::bool_constant || std::is_floating_point_v || std::is_enum_v> + : std::bool_constant || std::is_floating_point_v || IntegralEnum> {}; template<> @@ -104,6 +106,20 @@ struct adl_serializer { return T::from_json(std::forward(j)); } + + template + requires IntegralEnum + static void to_json(Json && json, const T & value) + { + json = static_cast>(value); + } + + template + requires IntegralEnum + static void from_json(const Json & json, T & value) + { + value = static_cast(json.template get>()); + } }; template diff --git a/lix/libutil/logging.hh b/lix/libutil/logging.hh index 97a00808b..c97acb399 100644 --- a/lix/libutil/logging.hh +++ b/lix/libutil/logging.hh @@ -44,6 +44,9 @@ typedef enum { actBuildWaiting = 111, } ActivityType; +template<> +struct json::is_integral_enum : std::true_type {}; + typedef enum { /** Fields: * 0: int: bytes linked @@ -82,6 +85,9 @@ typedef enum { resPostBuildLogLine = 107, } ResultType; +template<> +struct json::is_integral_enum : std::true_type {}; + typedef uint64_t ActivityId; struct LoggerSettings : Config diff --git a/meson.build b/meson.build index 8a23590e1..430a21dda 100644 --- a/meson.build +++ b/meson.build @@ -518,6 +518,9 @@ add_project_arguments( '-Wdeprecated-copy', '-Wignored-qualifiers', '-Werror=suggest-override', + # don't let nlohmann automatically cast enums to ints. we don't set this + # inside libutil headers to not force this behavior on users of libutil. + '-DJSON_DISABLE_ENUM_SERIALIZATION=1', language : 'cpp', ) diff --git a/tests/unit/libutil/config.cc b/tests/unit/libutil/config.cc index 447491bde..8fe1b18b5 100644 --- a/tests/unit/libutil/config.cc +++ b/tests/unit/libutil/config.cc @@ -1,5 +1,7 @@ #include "lix/libutil/config.hh" #include "lix/libutil/args.hh" +#include "lix/libutil/deprecated-features.hh" +#include "lix/libutil/experimental-features.hh" #include "lix/libutil/file-system.hh" #include "lix/libutil/environment-variables.hh" #include "lix/libutil/json.hh" @@ -323,4 +325,23 @@ namespace nix { ASSERT_THROW(config.applyConfig("value == key"), UsageError); ASSERT_THROW(config.applyConfig("value "), UsageError); } + +/* ---------------------------------------------------------------------------- + * Config enums + * --------------------------------------------------------------------------*/ +TEST(Config, regression_738) +{ + // feature enums serialized as integers because bitmaks type serializers + // were forgotten and nlohmann casted then to ints automatically. -sigh- + ASSERT_EQ(JSON(ExperimentalFeature::AutoAllocateUids).dump(), "\"auto-allocate-uids\""); + ASSERT_EQ( + JSON((ExperimentalFeatures{} | ExperimentalFeature::AutoAllocateUids)).dump(), + "[\"auto-allocate-uids\"]" + ); + + ASSERT_EQ(JSON(DeprecatedFeature::AncientLet).dump(), "\"ancient-let\""); + ASSERT_EQ( + JSON((DeprecatedFeatures{} | DeprecatedFeature::AncientLet)).dump(), "[\"ancient-let\"]" + ); +} }