formats.ini: disable merging as list by default

Previously, setting listsAsDuplicateKeys or listToValue would make it so
merging these treat all values as lists, by coercing non-lists via
lib.singleton. Some programs (such as gamemode; see #345121), allow some
values to be repeated but not others, which can lead to unexpected
behavior when non-list values are merged like this rather than throwing
an error.

This now makes that behavior opt-in via the mergeAsList option. Setting
mergeAsList (to either true or false) without setting either
listsAsDuplicateKeys or listToValue is an error, since lists are
meaningless in this case.
This commit is contained in:
MithicSpirit 2024-10-03 21:28:24 -04:00
parent e5e2a4b18e
commit e14483d6a6
No known key found for this signature in database
GPG Key ID: 0EA043551EBD3C2E
3 changed files with 171 additions and 9 deletions

View File

@ -566,6 +566,11 @@
- The `rustic` package was upgrade to `0.9.0`, which contains [breaking changes to the config file format](https://github.com/rustic-rs/rustic/releases/tag/v0.9.0). - The `rustic` package was upgrade to `0.9.0`, which contains [breaking changes to the config file format](https://github.com/rustic-rs/rustic/releases/tag/v0.9.0).
- `pkgs.formats.ini` and `pkgs.formats.iniWithGlobalSection` with
`listsAsDuplicateKeys` or `listToValue` no longer merge non-list values into
lists by default. Backwards-compatible behavior can be enabled with
`atomsCoercedToLists`.
## Other Notable Changes {#sec-release-24.11-notable-changes} ## Other Notable Changes {#sec-release-24.11-notable-changes}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View File

@ -109,18 +109,21 @@ rec {
singleIniAtom = nullOr (oneOf [ bool int float str ]) // { singleIniAtom = nullOr (oneOf [ bool int float str ]) // {
description = "INI atom (null, bool, int, float or string)"; description = "INI atom (null, bool, int, float or string)";
}; };
iniAtom = { listsAsDuplicateKeys, listToValue }: iniAtom = { listsAsDuplicateKeys, listToValue, atomsCoercedToLists }:
let
singleIniAtomOr = if atomsCoercedToLists then coercedTo singleIniAtom lib.singleton else either singleIniAtom;
in
if listsAsDuplicateKeys then if listsAsDuplicateKeys then
coercedTo singleIniAtom lib.singleton (listOf singleIniAtom) // { singleIniAtomOr (listOf singleIniAtom) // {
description = singleIniAtom.description + " or a list of them for duplicate keys"; description = singleIniAtom.description + " or a list of them for duplicate keys";
} }
else if listToValue != null then else if listToValue != null then
coercedTo singleIniAtom lib.singleton (nonEmptyListOf singleIniAtom) // { singleIniAtomOr (nonEmptyListOf singleIniAtom) // {
description = singleIniAtom.description + " or a non-empty list of them"; description = singleIniAtom.description + " or a non-empty list of them";
} }
else else
singleIniAtom; singleIniAtom;
iniSection = { listsAsDuplicateKeys, listToValue }@args: iniSection = { listsAsDuplicateKeys, listToValue, atomsCoercedToLists }@args:
attrsOf (iniAtom args) // { attrsOf (iniAtom args) // {
description = "section of an INI file (attrs of " + (iniAtom args).description + ")"; description = "section of an INI file (attrs of " + (iniAtom args).description + ")";
}; };
@ -133,18 +136,26 @@ rec {
# Alternative to listsAsDuplicateKeys, converts list to non-list # Alternative to listsAsDuplicateKeys, converts list to non-list
# listToValue :: [IniAtom] -> IniAtom # listToValue :: [IniAtom] -> IniAtom
listToValue ? null, listToValue ? null,
# Merge multiple instances of the same key into a list
atomsCoercedToLists ? null,
... ...
}@args: }@args:
assert listsAsDuplicateKeys -> listToValue == null; assert listsAsDuplicateKeys -> listToValue == null;
assert atomsCoercedToLists != null -> (listsAsDuplicateKeys || listToValue != null);
let
atomsCoercedToLists' = if atomsCoercedToLists == null then false else atomsCoercedToLists;
in
{ {
type = lib.types.attrsOf (iniSection { listsAsDuplicateKeys = listsAsDuplicateKeys; listToValue = listToValue; }); type = lib.types.attrsOf (
iniSection { inherit listsAsDuplicateKeys listToValue; atomsCoercedToLists = atomsCoercedToLists'; }
);
generate = name: value: generate = name: value:
lib.pipe value lib.pipe value
[ [
(lib.mapAttrs (_: maybeToList listToValue)) (lib.mapAttrs (_: maybeToList listToValue))
(lib.generators.toINI (removeAttrs args ["listToValue"])) (lib.generators.toINI (removeAttrs args ["listToValue" "atomsCoercedToLists"]))
(pkgs.writeText name) (pkgs.writeText name)
]; ];
}; };
@ -155,26 +166,34 @@ rec {
# Alternative to listsAsDuplicateKeys, converts list to non-list # Alternative to listsAsDuplicateKeys, converts list to non-list
# listToValue :: [IniAtom] -> IniAtom # listToValue :: [IniAtom] -> IniAtom
listToValue ? null, listToValue ? null,
# Merge multiple instances of the same key into a list
atomsCoercedToLists ? null,
... ...
}@args: }@args:
assert listsAsDuplicateKeys -> listToValue == null; assert listsAsDuplicateKeys -> listToValue == null;
assert atomsCoercedToLists != null -> (listsAsDuplicateKeys || listToValue != null);
let
atomsCoercedToLists' = if atomsCoercedToLists == null then false else atomsCoercedToLists;
in
{ {
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
sections = lib.mkOption rec { sections = lib.mkOption rec {
type = lib.types.attrsOf (iniSection { listsAsDuplicateKeys = listsAsDuplicateKeys; listToValue = listToValue; }); type = lib.types.attrsOf (
iniSection { inherit listsAsDuplicateKeys listToValue; atomsCoercedToLists = atomsCoercedToLists'; }
);
default = {}; default = {};
description = type.description; description = type.description;
}; };
globalSection = lib.mkOption rec { globalSection = lib.mkOption rec {
type = iniSection { listsAsDuplicateKeys = listsAsDuplicateKeys; listToValue = listToValue; }; type = iniSection { inherit listsAsDuplicateKeys listToValue; atomsCoercedToLists = atomsCoercedToLists'; };
default = {}; default = {};
description = "global " + type.description; description = "global " + type.description;
}; };
}; };
}; };
generate = name: { sections ? {}, globalSection ? {}, ... }: generate = name: { sections ? {}, globalSection ? {}, ... }:
pkgs.writeText name (lib.generators.toINIWithGlobalSection (removeAttrs args ["listToValue"]) pkgs.writeText name (lib.generators.toINIWithGlobalSection (removeAttrs args ["listToValue" "atomsCoercedToLists"])
{ {
globalSection = maybeToList listToValue globalSection; globalSection = maybeToList listToValue globalSection;
sections = lib.mapAttrs (_: maybeToList listToValue) sections; sections = lib.mapAttrs (_: maybeToList listToValue) sections;
@ -186,6 +205,7 @@ rec {
atom = iniAtom { atom = iniAtom {
listsAsDuplicateKeys = listsAsDuplicateKeys; listsAsDuplicateKeys = listsAsDuplicateKeys;
listToValue = null; listToValue = null;
atomsCoercedToLists = false;
}; };
in attrsOf (attrsOf (either atom (attrsOf atom))); in attrsOf (attrsOf (either atom (attrsOf atom)));

View File

@ -222,6 +222,67 @@ in runBuildTests {
''; '';
}; };
iniCoercedDuplicateKeys = shouldPass rec {
format = formats.ini {
listsAsDuplicateKeys = true;
atomsCoercedToLists = true;
};
input = format.type.merge [ ] [
{
file = "format-test-inner-iniCoercedDuplicateKeys";
value = { foo = { bar = 1; }; };
}
{
file = "format-test-inner-iniCoercedDuplicateKeys";
value = { foo = { bar = 2; }; };
}
];
expected = ''
[foo]
bar=1
bar=2
'';
};
iniCoercedListToValue = shouldPass rec {
format = formats.ini {
listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
atomsCoercedToLists = true;
};
input = format.type.merge [ ] [
{
file = "format-test-inner-iniCoercedListToValue";
value = { foo = { bar = 1; }; };
}
{
file = "format-test-inner-iniCoercedListToValue";
value = { foo = { bar = 2; }; };
}
];
expected = ''
[foo]
bar=1, 2
'';
};
iniCoercedNoLists = shouldFail {
format = formats.ini { atomsCoercedToLists = true; };
input = {
foo = {
bar = 1;
};
};
};
iniNoCoercedNoLists = shouldFail {
format = formats.ini { atomsCoercedToLists = false; };
input = {
foo = {
bar = 1;
};
};
};
iniWithGlobalNoSections = shouldPass { iniWithGlobalNoSections = shouldPass {
format = formats.iniWithGlobalSection {}; format = formats.iniWithGlobalSection {};
input = {}; input = {};
@ -317,6 +378,82 @@ in runBuildTests {
''; '';
}; };
iniWithGlobalCoercedDuplicateKeys = shouldPass rec {
format = formats.iniWithGlobalSection {
listsAsDuplicateKeys = true;
atomsCoercedToLists = true;
};
input = format.type.merge [ ] [
{
file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
value = {
globalSection = { baz = 4; };
sections = { foo = { bar = 1; }; };
};
}
{
file = "format-test-inner-iniWithGlobalCoercedDuplicateKeys";
value = {
globalSection = { baz = 3; };
sections = { foo = { bar = 2; }; };
};
}
];
expected = ''
baz=3
baz=4
[foo]
bar=2
bar=1
'';
};
iniWithGlobalCoercedListToValue = shouldPass rec {
format = formats.iniWithGlobalSection {
listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault { });
atomsCoercedToLists = true;
};
input = format.type.merge [ ] [
{
file = "format-test-inner-iniWithGlobalCoercedListToValue";
value = {
globalSection = { baz = 4; };
sections = { foo = { bar = 1; }; };
};
}
{
file = "format-test-inner-iniWithGlobalCoercedListToValue";
value = {
globalSection = { baz = 3; };
sections = { foo = { bar = 2; }; };
};
}
];
expected = ''
baz=3, 4
[foo]
bar=2, 1
'';
};
iniWithGlobalCoercedNoLists = shouldFail {
format = formats.iniWithGlobalSection { atomsCoercedToLists = true; };
input = {
globalSection = { baz = 4; };
foo = { bar = 1; };
};
};
iniWithGlobalNoCoercedNoLists = shouldFail {
format = formats.iniWithGlobalSection { atomsCoercedToLists = false; };
input = {
globalSection = { baz = 4; };
foo = { bar = 1; };
};
};
keyValueAtoms = shouldPass { keyValueAtoms = shouldPass {
format = formats.keyValue {}; format = formats.keyValue {};
input = { input = {