Skip to content

Commit dc769d7

Browse files
committed
libexpr: Canonicalize TOML timestamps for toml11 > 4.0
This addresses several changes from toml11 4.0 bump in nixpkgs [1]. 1. Added more regression tests for timestamp formats. Special attention needs to be paid to the precision of the subsecond range for local-time. Prior versions select the closest (upwards) multiple of 3 with a hard cap of 9 digits. 2. Normalize local datetime and offset datetime to always use the uppercase separator `T`. This is actually the issue surfaced in [2]. This canonicalization is basically a requirement by (a certain reading) of rfc3339 section 5.6 [3]. 3. If using toml11 >= 4.0 also keep the old behavior wrt to the number of digits used for subsecond part of the local-time. Newer versions cap it at 6 digits unconditionally. [1]: https://www.github.com/NixOS/nixpkgs/pull/331649 [2]: https://www.github.com/NixOS/nix/issues/11441 [3]: https://datatracker.ietf.org/doc/html/rfc3339
1 parent d8fc55a commit dc769d7

File tree

2 files changed

+99
-1
lines changed

2 files changed

+99
-1
lines changed

src/libexpr/meson.build

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ toml11 = dependency(
7171
method : 'cmake',
7272
include_type : 'system',
7373
)
74+
75+
configdata_priv.set(
76+
'HAVE_TOML11_4',
77+
toml11.version().version_compare('>= 4.0.0').to_int(),
78+
)
79+
7480
deps_other += toml11
7581

7682
config_priv_h = configure_file(

src/libexpr/primops/fromTOML.cc

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,91 @@
11
#include "nix/expr/primops.hh"
22
#include "nix/expr/eval-inline.hh"
33

4+
#include "expr-config-private.hh"
5+
46
#include <sstream>
57

68
#include <toml.hpp>
79

810
namespace nix {
911

12+
#if HAVE_TOML11_4
13+
14+
/**
15+
* This is what toml11 < 4.0 did when choosing the subsecond precision.
16+
* TOML 1.0.0 spec doesn't define how sub-millisecond ranges should be handled and calls it
17+
* implementation defined behavior. For a lack of a better choice we stick with what older versions
18+
* of toml11 did [1].
19+
*
20+
* [1]: https://github.com/ToruNiina/toml11/blob/dcfe39a783a94e8d52c885e5883a6fbb21529019/toml/datetime.hpp#L282
21+
*/
22+
static size_t normalizeSubsecondPrecision(toml::local_time lt)
23+
{
24+
auto millis = lt.millisecond;
25+
auto micros = lt.microsecond;
26+
auto nanos = lt.nanosecond;
27+
if (millis != 0 || micros != 0 || nanos != 0) {
28+
if (micros != 0 || nanos != 0) {
29+
if (nanos != 0)
30+
return 9;
31+
return 6;
32+
}
33+
return 3;
34+
}
35+
return 0;
36+
}
37+
38+
/**
39+
* Normalize date/time formats to serialize to the same strings as versions prior to toml11 4.0.
40+
*
41+
* Several things to consider:
42+
*
43+
* 1. Sub-millisecond range is represented the same way as in toml11 versions prior to 4.0. Precisioun is rounded
44+
* towards the next multiple of 3 or capped at 9 digits.
45+
* 2. Seconds must be specified. This may become optional in (yet unreleased) TOML 1.1.0, but 1.0.0 defined local time
46+
* in terms of RFC3339 [1].
47+
* 3. date-time separator (`t`, `T` or space ` `) is canonicalized to an upper T. This is compliant with RFC3339
48+
* [1] 5.6:
49+
* > Applications that generate this format SHOULD use upper case letters.
50+
*
51+
* [1]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
52+
*/
53+
static void normalizeDatetimeFormat(toml::value & t)
54+
{
55+
if (t.is_local_datetime()) {
56+
auto & ldt = t.as_local_datetime();
57+
t.as_local_datetime_fmt() = {
58+
.delimiter = toml::datetime_delimiter_kind::upper_T,
59+
// https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
60+
.has_seconds = true, // Mandated by TOML 1.0.0
61+
.subsecond_precision = normalizeSubsecondPrecision(ldt.time),
62+
};
63+
return;
64+
}
65+
66+
if (t.is_offset_datetime()) {
67+
auto & odt = t.as_offset_datetime();
68+
t.as_offset_datetime_fmt() = {
69+
.delimiter = toml::datetime_delimiter_kind::upper_T,
70+
// https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
71+
.has_seconds = true, // Mandated by TOML 1.0.0
72+
.subsecond_precision = normalizeSubsecondPrecision(odt.time),
73+
};
74+
return;
75+
}
76+
77+
if (t.is_local_time()) {
78+
auto & lt = t.as_local_time();
79+
t.as_local_time_fmt() = {
80+
.has_seconds = true, // Mandated by TOML 1.0.0
81+
.subsecond_precision = normalizeSubsecondPrecision(lt),
82+
};
83+
return;
84+
}
85+
}
86+
87+
#endif
88+
1089
static void prim_fromTOML(EvalState & state, const PosIdx pos, Value ** args, Value & val)
1190
{
1291
auto toml = state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.fromTOML");
@@ -53,6 +132,9 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value ** args, Va
53132
case toml::value_t::local_date:
54133
case toml::value_t::local_time: {
55134
if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) {
135+
#if HAVE_TOML11_4
136+
normalizeDatetimeFormat(t);
137+
#endif
56138
auto attrs = state.buildBindings(2);
57139
attrs.alloc("_type").mkString("timestamp");
58140
std::ostringstream s;
@@ -72,7 +154,17 @@ static void prim_fromTOML(EvalState & state, const PosIdx pos, Value ** args, Va
72154
};
73155

74156
try {
75-
visit(visit, val, toml::parse(tomlStream, "fromTOML" /* the "filename" */));
157+
visit(
158+
visit,
159+
val,
160+
toml::parse(
161+
tomlStream,
162+
"fromTOML" /* the "filename" */
163+
#if HAVE_TOML11_4
164+
,
165+
toml::spec::v(1, 0, 0) // Be explicit that we are parsing TOML 1.0.0 without extensions
166+
#endif
167+
));
76168
} catch (std::exception & e) { // TODO: toml::syntax_error
77169
state.error<EvalError>("while parsing TOML: %s", e.what()).atPos(pos).debugThrow();
78170
}

0 commit comments

Comments
 (0)