From 3c78920f7358957dba37a379e9d0b062dd3192e2 Mon Sep 17 00:00:00 2001
From: Andrea Bedini <andrea@andreabedini.com>
Date: Fri, 9 Jun 2023 17:53:18 +0800
Subject: [PATCH] Parse TOML timestamps (#8120)

Currently `fromTOML` throws an exception when encountering a timestamp
since the Nix language lacks a way to represent them.

This patch changes this beaviour and makes `fromTOML` parse timestamps as
attrsets of the format

  { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }

This is guarded by an experimental feature flag to leave room for iterating on the representation.
---
 src/libexpr/primops/fromTOML.cc               |  16 ++-
 src/libutil/experimental-features.cc          |  11 +-
 src/libutil/experimental-features.hh          |   1 +
 tests/lang/eval-fail-fromTOML-timestamps.nix  | 130 ++++++++++++++++++
 tests/lang/eval-okay-fromTOML-timestamps.exp  |   1 +
 .../lang/eval-okay-fromTOML-timestamps.flags  |   1 +
 tests/lang/eval-okay-fromTOML-timestamps.nix  | 130 ++++++++++++++++++
 7 files changed, 286 insertions(+), 4 deletions(-)
 create mode 100644 tests/lang/eval-fail-fromTOML-timestamps.nix
 create mode 100644 tests/lang/eval-okay-fromTOML-timestamps.exp
 create mode 100644 tests/lang/eval-okay-fromTOML-timestamps.flags
 create mode 100644 tests/lang/eval-okay-fromTOML-timestamps.nix

diff --git a/src/libexpr/primops/fromTOML.cc b/src/libexpr/primops/fromTOML.cc
index 8a5231781..e2a8b3c3a 100644
--- a/src/libexpr/primops/fromTOML.cc
+++ b/src/libexpr/primops/fromTOML.cc
@@ -3,6 +3,8 @@
 
 #include "../../toml11/toml.hpp"
 
+#include <sstream>
+
 namespace nix {
 
 static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, Value & val)
@@ -58,8 +60,18 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value * * args, V
             case toml::value_t::offset_datetime:
             case toml::value_t::local_date:
             case toml::value_t::local_time:
-                // We fail since Nix doesn't have date and time types
-                throw std::runtime_error("Dates and times are not supported");
+                {
+                    if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) {
+                        auto attrs = state.buildBindings(2);
+                        attrs.alloc("_type").mkString("timestamp");
+                        std::ostringstream s;
+                        s << t;
+                        attrs.alloc("value").mkString(s.str());
+                        v.mkAttrs(attrs);
+                    } else {
+                        throw std::runtime_error("Dates and times are not supported");
+                    }
+                }
                 break;;
             case toml::value_t::empty:
                 v.mkNull();
diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc
index 4bf545d7b..c4642d333 100644
--- a/src/libutil/experimental-features.cc
+++ b/src/libutil/experimental-features.cc
@@ -12,7 +12,7 @@ struct ExperimentalFeatureDetails
     std::string_view description;
 };
 
-constexpr std::array<ExperimentalFeatureDetails, 13> xpFeatureDetails = {{
+constexpr std::array<ExperimentalFeatureDetails, 14> xpFeatureDetails = {{
     {
         .tag = Xp::CaDerivations,
         .name = "ca-derivations",
@@ -214,6 +214,13 @@ constexpr std::array<ExperimentalFeatureDetails, 13> xpFeatureDetails = {{
                 derivations that are themselves derivations outputs.
         )",
     },
+    {
+        .tag = Xp::ParseTomlTimestamps,
+        .name = "parse-toml-timestamps",
+        .description = R"(
+            Allow parsing of timestamps in builtins.fromTOML.
+        )",
+    },
 }};
 
 static_assert(
@@ -248,7 +255,7 @@ std::string_view showExperimentalFeature(const ExperimentalFeature tag)
     return xpFeatureDetails[(size_t)tag].name;
 }
 
-nlohmann::json documentExperimentalFeatures() 
+nlohmann::json documentExperimentalFeatures()
 {
     StringMap res;
     for (auto & xpFeature : xpFeatureDetails)
diff --git a/src/libutil/experimental-features.hh b/src/libutil/experimental-features.hh
index 409100592..892c6c371 100644
--- a/src/libutil/experimental-features.hh
+++ b/src/libutil/experimental-features.hh
@@ -30,6 +30,7 @@ enum struct ExperimentalFeature
     DiscardReferences,
     DaemonTrustOverride,
     DynamicDerivations,
+    ParseTomlTimestamps,
 };
 
 /**
diff --git a/tests/lang/eval-fail-fromTOML-timestamps.nix b/tests/lang/eval-fail-fromTOML-timestamps.nix
new file mode 100644
index 000000000..74cff9470
--- /dev/null
+++ b/tests/lang/eval-fail-fromTOML-timestamps.nix
@@ -0,0 +1,130 @@
+builtins.fromTOML ''
+  key = "value"
+  bare_key = "value"
+  bare-key = "value"
+  1234 = "value"
+
+  "127.0.0.1" = "value"
+  "character encoding" = "value"
+  "ʎǝʞ" = "value"
+  'key2' = "value"
+  'quoted "value"' = "value"
+
+  name = "Orange"
+
+  physical.color = "orange"
+  physical.shape = "round"
+  site."google.com" = true
+
+  # This is legal according to the spec, but cpptoml doesn't handle it.
+  #a.b.c = 1
+  #a.d = 2
+
+  str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
+
+  int1 = +99
+  int2 = 42
+  int3 = 0
+  int4 = -17
+  int5 = 1_000
+  int6 = 5_349_221
+  int7 = 1_2_3_4_5
+
+  hex1 = 0xDEADBEEF
+  hex2 = 0xdeadbeef
+  hex3 = 0xdead_beef
+
+  oct1 = 0o01234567
+  oct2 = 0o755
+
+  bin1 = 0b11010110
+
+  flt1 = +1.0
+  flt2 = 3.1415
+  flt3 = -0.01
+  flt4 = 5e+22
+  flt5 = 1e6
+  flt6 = -2E-2
+  flt7 = 6.626e-34
+  flt8 = 9_224_617.445_991_228_313
+
+  bool1 = true
+  bool2 = false
+
+  odt1 = 1979-05-27T07:32:00Z
+  odt2 = 1979-05-27T00:32:00-07:00
+  odt3 = 1979-05-27T00:32:00.999999-07:00
+  odt4 = 1979-05-27 07:32:00Z
+  ldt1 = 1979-05-27T07:32:00
+  ldt2 = 1979-05-27T00:32:00.999999
+  ld1 = 1979-05-27
+  lt1 = 07:32:00
+  lt2 = 00:32:00.999999
+
+  arr1 = [ 1, 2, 3 ]
+  arr2 = [ "red", "yellow", "green" ]
+  arr3 = [ [ 1, 2 ], [3, 4, 5] ]
+  arr4 = [ "all", 'strings', """are the same""", ''''type'''']
+  arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
+
+  arr7 = [
+    1, 2, 3
+  ]
+
+  arr8 = [
+    1,
+    2, # this is ok
+  ]
+
+  [table-1]
+  key1 = "some string"
+  key2 = 123
+
+
+  [table-2]
+  key1 = "another string"
+  key2 = 456
+
+  [dog."tater.man"]
+  type.name = "pug"
+
+  [a.b.c]
+  [ d.e.f ]
+  [ g .  h  . i ]
+  [ j . "ʞ" . 'l' ]
+  [x.y.z.w]
+
+  name = { first = "Tom", last = "Preston-Werner" }
+  point = { x = 1, y = 2 }
+  animal = { type.name = "pug" }
+
+  [[products]]
+  name = "Hammer"
+  sku = 738594937
+
+  [[products]]
+
+  [[products]]
+  name = "Nail"
+  sku = 284758393
+  color = "gray"
+
+  [[fruit]]
+    name = "apple"
+
+    [fruit.physical]
+      color = "red"
+      shape = "round"
+
+    [[fruit.variety]]
+      name = "red delicious"
+
+    [[fruit.variety]]
+      name = "granny smith"
+
+  [[fruit]]
+    name = "banana"
+
+    [[fruit.variety]]
+      name = "plantain"
+''
diff --git a/tests/lang/eval-okay-fromTOML-timestamps.exp b/tests/lang/eval-okay-fromTOML-timestamps.exp
new file mode 100644
index 000000000..08b3c69a6
--- /dev/null
+++ b/tests/lang/eval-okay-fromTOML-timestamps.exp
@@ -0,0 +1 @@
+{ "1234" = "value"; "127.0.0.1" = "value"; a = { b = { c = { }; }; }; arr1 = [ 1 2 3 ]; arr2 = [ "red" "yellow" "green" ]; arr3 = [ [ 1 2 ] [ 3 4 5 ] ]; arr4 = [ "all" "strings" "are the same" "type" ]; arr5 = [ [ 1 2 ] [ "a" "b" "c" ] ]; arr7 = [ 1 2 3 ]; arr8 = [ 1 2 ]; bare-key = "value"; bare_key = "value"; bin1 = 214; bool1 = true; bool2 = false; "character encoding" = "value"; d = { e = { f = { }; }; }; dog = { "tater.man" = { type = { name = "pug"; }; }; }; flt1 = 1; flt2 = 3.1415; flt3 = -0.01; flt4 = 5e+22; flt5 = 1e+06; flt6 = -0.02; flt7 = 6.626e-34; flt8 = 9.22462e+06; fruit = [ { name = "apple"; physical = { color = "red"; shape = "round"; }; variety = [ { name = "red delicious"; } { name = "granny smith"; } ]; } { name = "banana"; variety = [ { name = "plantain"; } ]; } ]; g = { h = { i = { }; }; }; hex1 = 3735928559; hex2 = 3735928559; hex3 = 3735928559; int1 = 99; int2 = 42; int3 = 0; int4 = -17; int5 = 1000; int6 = 5349221; int7 = 12345; j = { "ʞ" = { l = { }; }; }; key = "value"; key2 = "value"; ld1 = { _type = "timestamp"; value = "1979-05-27"; }; ldt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00"; }; ldt2 = { _type = "timestamp"; value = "1979-05-27T00:32:00.999999"; }; lt1 = { _type = "timestamp"; value = "07:32:00"; }; lt2 = { _type = "timestamp"; value = "00:32:00.999999"; }; name = "Orange"; oct1 = 342391; oct2 = 493; odt1 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; odt2 = { _type = "timestamp"; value = "1979-05-27T00:32:00-07:00"; }; odt3 = { _type = "timestamp"; value = "1979-05-27T00:32:00.999999-07:00"; }; odt4 = { _type = "timestamp"; value = "1979-05-27T07:32:00Z"; }; physical = { color = "orange"; shape = "round"; }; products = [ { name = "Hammer"; sku = 738594937; } { } { color = "gray"; name = "Nail"; sku = 284758393; } ]; "quoted \"value\"" = "value"; site = { "google.com" = true; }; str = "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."; table-1 = { key1 = "some string"; key2 = 123; }; table-2 = { key1 = "another string"; key2 = 456; }; x = { y = { z = { w = { animal = { type = { name = "pug"; }; }; name = { first = "Tom"; last = "Preston-Werner"; }; point = { x = 1; y = 2; }; }; }; }; }; "ʎǝʞ" = "value"; }
diff --git a/tests/lang/eval-okay-fromTOML-timestamps.flags b/tests/lang/eval-okay-fromTOML-timestamps.flags
new file mode 100644
index 000000000..9ed39dc6b
--- /dev/null
+++ b/tests/lang/eval-okay-fromTOML-timestamps.flags
@@ -0,0 +1 @@
+--extra-experimental-features parse-toml-timestamps
diff --git a/tests/lang/eval-okay-fromTOML-timestamps.nix b/tests/lang/eval-okay-fromTOML-timestamps.nix
new file mode 100644
index 000000000..74cff9470
--- /dev/null
+++ b/tests/lang/eval-okay-fromTOML-timestamps.nix
@@ -0,0 +1,130 @@
+builtins.fromTOML ''
+  key = "value"
+  bare_key = "value"
+  bare-key = "value"
+  1234 = "value"
+
+  "127.0.0.1" = "value"
+  "character encoding" = "value"
+  "ʎǝʞ" = "value"
+  'key2' = "value"
+  'quoted "value"' = "value"
+
+  name = "Orange"
+
+  physical.color = "orange"
+  physical.shape = "round"
+  site."google.com" = true
+
+  # This is legal according to the spec, but cpptoml doesn't handle it.
+  #a.b.c = 1
+  #a.d = 2
+
+  str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
+
+  int1 = +99
+  int2 = 42
+  int3 = 0
+  int4 = -17
+  int5 = 1_000
+  int6 = 5_349_221
+  int7 = 1_2_3_4_5
+
+  hex1 = 0xDEADBEEF
+  hex2 = 0xdeadbeef
+  hex3 = 0xdead_beef
+
+  oct1 = 0o01234567
+  oct2 = 0o755
+
+  bin1 = 0b11010110
+
+  flt1 = +1.0
+  flt2 = 3.1415
+  flt3 = -0.01
+  flt4 = 5e+22
+  flt5 = 1e6
+  flt6 = -2E-2
+  flt7 = 6.626e-34
+  flt8 = 9_224_617.445_991_228_313
+
+  bool1 = true
+  bool2 = false
+
+  odt1 = 1979-05-27T07:32:00Z
+  odt2 = 1979-05-27T00:32:00-07:00
+  odt3 = 1979-05-27T00:32:00.999999-07:00
+  odt4 = 1979-05-27 07:32:00Z
+  ldt1 = 1979-05-27T07:32:00
+  ldt2 = 1979-05-27T00:32:00.999999
+  ld1 = 1979-05-27
+  lt1 = 07:32:00
+  lt2 = 00:32:00.999999
+
+  arr1 = [ 1, 2, 3 ]
+  arr2 = [ "red", "yellow", "green" ]
+  arr3 = [ [ 1, 2 ], [3, 4, 5] ]
+  arr4 = [ "all", 'strings', """are the same""", ''''type'''']
+  arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
+
+  arr7 = [
+    1, 2, 3
+  ]
+
+  arr8 = [
+    1,
+    2, # this is ok
+  ]
+
+  [table-1]
+  key1 = "some string"
+  key2 = 123
+
+
+  [table-2]
+  key1 = "another string"
+  key2 = 456
+
+  [dog."tater.man"]
+  type.name = "pug"
+
+  [a.b.c]
+  [ d.e.f ]
+  [ g .  h  . i ]
+  [ j . "ʞ" . 'l' ]
+  [x.y.z.w]
+
+  name = { first = "Tom", last = "Preston-Werner" }
+  point = { x = 1, y = 2 }
+  animal = { type.name = "pug" }
+
+  [[products]]
+  name = "Hammer"
+  sku = 738594937
+
+  [[products]]
+
+  [[products]]
+  name = "Nail"
+  sku = 284758393
+  color = "gray"
+
+  [[fruit]]
+    name = "apple"
+
+    [fruit.physical]
+      color = "red"
+      shape = "round"
+
+    [[fruit.variety]]
+      name = "red delicious"
+
+    [[fruit.variety]]
+      name = "granny smith"
+
+  [[fruit]]
+    name = "banana"
+
+    [[fruit.variety]]
+      name = "plantain"
+''