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
This commit is contained in:
eldritch horrors
2025-03-25 10:44:52 +01:00
parent e5b9c86e59
commit c3929c78f3
12 changed files with 119 additions and 2 deletions
+9
View File
@@ -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.
+4
View File
@@ -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<TrustedFlag> : std::true_type {};
struct BuildResult;
struct KeyedBuildResult;
+16
View File
@@ -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<StringSet>());
}
}
+8
View File
@@ -4,6 +4,7 @@
#include "lix/libutil/error.hh"
#include "lix/libutil/json-fwd.hh"
#include "lix/libutil/types.hh"
#include <type_traits>
namespace nix {
@@ -19,6 +20,10 @@ enum struct DeprecatedFeature
NumDepFeatures, // number of available deprecated features, do not use
};
template<>
struct json::avoids_null<DeprecatedFeature> : 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.
+5
View File
@@ -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 <sys/stat.h>
#include <fcntl.h>
#include <system_error>
#include <type_traits>
namespace nix {
@@ -42,6 +44,9 @@ typedef enum {
lvlVomit
} Verbosity;
template<>
struct json::is_integral_enum<Verbosity> : std::true_type {};
Verbosity verbosityFromIntClamped(int val);
/**
+16
View File
@@ -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<StringSet>());
}
}
+8
View File
@@ -4,6 +4,7 @@
#include "lix/libutil/error.hh"
#include "lix/libutil/json-fwd.hh"
#include "lix/libutil/types.hh"
#include <type_traits>
namespace nix {
@@ -16,6 +17,10 @@ enum struct ExperimentalFeature
NumXpFeatures, // number of available experimental features, do not use
};
template<>
struct json::avoids_null<ExperimentalFeature> : 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.
+6 -1
View File
@@ -2,6 +2,7 @@
///@file Lix-specific JSON handling (forward declarations only).
#include <nlohmann/json_fwd.hpp>
#include <type_traits>
namespace nix {
@@ -16,7 +17,11 @@ template<typename T>
struct avoids_null;
template<typename T>
struct is_integral_enum;
struct is_integral_enum : std::false_type
{};
template<typename T>
concept IntegralEnum = is_integral_enum<T>::value;
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
+17 -1
View File
@@ -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<T>` 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 <concepts>
@@ -30,7 +32,7 @@ namespace json {
*/
template<typename T>
struct avoids_null
: std::bool_constant<std::is_integral_v<T> || std::is_floating_point_v<T> || std::is_enum_v<T>>
: std::bool_constant<std::is_integral_v<T> || std::is_floating_point_v<T> || IntegralEnum<T>>
{};
template<>
@@ -104,6 +106,20 @@ struct adl_serializer<T, void>
{
return T::from_json(std::forward<Json>(j));
}
template<typename Json>
requires IntegralEnum<T>
static void to_json(Json && json, const T & value)
{
json = static_cast<std::underlying_type_t<T>>(value);
}
template<typename Json>
requires IntegralEnum<T>
static void from_json(const Json & json, T & value)
{
value = static_cast<T>(json.template get<std::underlying_type_t<T>>());
}
};
template<typename T>
+6
View File
@@ -44,6 +44,9 @@ typedef enum {
actBuildWaiting = 111,
} ActivityType;
template<>
struct json::is_integral_enum<ActivityType> : 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<ResultType> : std::true_type {};
typedef uint64_t ActivityId;
struct LoggerSettings : Config
+3
View File
@@ -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',
)
+21
View File
@@ -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\"]"
);
}
}