Skip to content

Commit 2898b9e

Browse files
xokdviumemilazy
authored andcommitted
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. [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 (cherry picked from commit dc769d72cb8ad22a0f89768682b5499a9d2b3d8b) Upstream-PR: NixOS/nix#13741 Change-Id: Iac4fbe5108be79be585e9670fa42dfd11f3c5e89
1 parent 19d9a87 commit 2898b9e

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

lix/libexpr/primops/fromTOML.cc

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,84 @@
66

77
namespace nix {
88

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

72153
try {
73-
visit(val, toml::parse(tomlStream, "fromTOML" /* the "filename" */));
154+
visit(
155+
val,
156+
toml::parse(
157+
tomlStream,
158+
"fromTOML" /* the "filename" */
159+
#if HAVE_TOML11_4
160+
,
161+
toml::spec::v(
162+
1, 0, 0
163+
) // Be explicit that we are parsing TOML 1.0.0 without extensions
164+
#endif
165+
)
166+
);
74167
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions) // TODO: toml::syntax_error
75168
state.ctx.errors.make<EvalError>("while parsing TOML: %s", e.what()).debugThrow();
76169
}

meson.build

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,9 @@ gtest = [
366366
]
367367

368368
toml11 = dependency('toml11', version : '>=3.7.0', required : true, method : 'cmake', include_type : 'system')
369+
configdata += {
370+
'HAVE_TOML11_4': toml11.version().version_compare('>= 4.0.0').to_int(),
371+
}
369372

370373
pegtl = dependency(
371374
'pegtl',

0 commit comments

Comments
 (0)