From b6d5670bcffebdd43352ea79b36135e35a8148d9 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Tue, 12 Aug 2025 16:11:54 +0300 Subject: [PATCH] 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: https://github.com/NixOS/nix/pull/13741 Change-Id: Iac4fbe5108be79be585e9670fa42dfd11f3c5e89 (cherry picked from commit 2898b9e7dc87d0c95c65aeebcbc7577a853d9f8d) --- lix/libexpr/primops/fromTOML.cc | 95 ++++++++++++++++++++++++++++++++- meson.build | 3 ++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/lix/libexpr/primops/fromTOML.cc b/lix/libexpr/primops/fromTOML.cc index c08334003..32edbb3ac 100644 --- a/lix/libexpr/primops/fromTOML.cc +++ b/lix/libexpr/primops/fromTOML.cc @@ -6,6 +6,84 @@ namespace nix { +#if HAVE_TOML11_4 + +/** + * This is what toml11 < 4.0 did when choosing the subsecond precision. + * TOML 1.0.0 spec doesn't define how sub-millisecond ranges should be handled and calls it + * implementation defined behavior. For a lack of a better choice we stick with what older versions + * of toml11 did [1]. + * + * [1]: + * https://github.com/ToruNiina/toml11/blob/dcfe39a783a94e8d52c885e5883a6fbb21529019/toml/datetime.hpp#L282 + */ +static size_t normalizeSubsecondPrecision(toml::local_time lt) +{ + auto millis = lt.millisecond; + auto micros = lt.microsecond; + auto nanos = lt.nanosecond; + if (millis != 0 || micros != 0 || nanos != 0) { + if (micros != 0 || nanos != 0) { + if (nanos != 0) { + return 9; + } + return 6; + } + return 3; + } + return 0; +} + +/** + * Normalize date/time formats to serialize to the same strings as versions prior to toml11 4.0. + * + * Several things to consider: + * + * 1. Sub-millisecond range is represented the same way as in toml11 versions prior to 4.0. + * Precision is rounded towards the next multiple of 3 or capped at 9 digits. + * 2. Seconds must be specified. This may become optional in (yet unreleased) TOML 1.1.0, but 1.0.0 + * defined local time in terms of RFC3339 [1]. + * 3. date-time separator (`t`, `T` or space ` `) is canonicalized to an upper T. This is compliant + * with RFC3339 [1] 5.6: > Applications that generate this format SHOULD use upper case letters. + * + * [1]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 + */ +static void normalizeDatetimeFormat(toml::value & t) +{ + if (t.is_local_datetime()) { + auto & ldt = t.as_local_datetime(); + t.as_local_datetime_fmt() = { + .delimiter = toml::datetime_delimiter_kind::upper_T, + // https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 + .has_seconds = true, // Mandated by TOML 1.0.0 + .subsecond_precision = normalizeSubsecondPrecision(ldt.time), + }; + return; + } + + if (t.is_offset_datetime()) { + auto & odt = t.as_offset_datetime(); + t.as_offset_datetime_fmt() = { + .delimiter = toml::datetime_delimiter_kind::upper_T, + // https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 + .has_seconds = true, // Mandated by TOML 1.0.0 + .subsecond_precision = normalizeSubsecondPrecision(odt.time), + }; + return; + } + + if (t.is_local_time()) { + auto & lt = t.as_local_time(); + t.as_local_time_fmt() = { + .has_seconds = true, // Mandated by TOML 1.0.0 + .subsecond_precision = normalizeSubsecondPrecision(lt), + }; + return; + } +} + +#endif + void prim_fromTOML(EvalState & state, Value ** args, Value & val) { auto toml = state.forceStringNoCtx( @@ -52,6 +130,9 @@ void prim_fromTOML(EvalState & state, Value ** args, Value & val) case toml::value_t::local_date: case toml::value_t::local_time: { if (experimentalFeatureSettings.isEnabled(Xp::ParseTomlTimestamps)) { +#if HAVE_TOML11_4 + normalizeDatetimeFormat(t); +#endif auto attrs = state.ctx.buildBindings(2); attrs.alloc("_type").mkString("timestamp"); std::ostringstream s; @@ -70,7 +151,19 @@ void prim_fromTOML(EvalState & state, Value ** args, Value & val) }; try { - visit(val, toml::parse(tomlStream, "fromTOML" /* the "filename" */)); + visit( + val, + toml::parse( + tomlStream, + "fromTOML" /* the "filename" */ +#if HAVE_TOML11_4 + , + toml::spec::v( + 1, 0, 0 + ) // Be explicit that we are parsing TOML 1.0.0 without extensions +#endif + ) + ); } catch (std::exception & e) { // NOLINT(lix-foreign-exceptions) // TODO: toml::syntax_error state.ctx.errors.make("while parsing TOML: %s", e.what()).debugThrow(); } diff --git a/meson.build b/meson.build index 576d847d1..3265d5abc 100644 --- a/meson.build +++ b/meson.build @@ -359,6 +359,9 @@ gtest = [ ] toml11 = dependency('toml11', version : '>=3.7.0', required : true, method : 'cmake', include_type : 'system') +configdata += { + 'HAVE_TOML11_4': toml11.version().version_compare('>= 4.0.0').to_int(), +} pegtl = dependency( 'pegtl',