Unverified Commit e14483d6 authored by MithicSpirit's avatar MithicSpirit
Browse files

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.
parent e5e2a4b1
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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).

- `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}

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

+137 −0
Original line number Diff line number Diff line
@@ -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 {
    format = formats.iniWithGlobalSection {};
    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 {
    format = formats.keyValue {};
    input = {