From 43f7704edcd01f2d33e392bd902a3cf822e60644 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Sat, 1 Nov 2025 00:36:15 +0300 Subject: [PATCH] libstore: Improve store-reference back-compat with IPv6 ZoneId literals This restores the pre-2.31 handling of ZoneID identifiers in store references. It's the only place we reasonably care about this back-compat. (cherry picked from commit 8dbc2475f710982a7a6b3f89ae686c586e6e2a42) --- .../ssh_unbracketed_ipv6_4.txt | 1 + .../ssh_unbracketed_ipv6_5.txt | 1 + .../ssh_unbracketed_ipv6_6.txt | 1 + .../ssh_unbracketed_ipv6_7.txt | 1 + .../ssh_unbracketed_ipv6_8.txt | 1 + .../ssh_unbracketed_ipv6_9.txt | 1 + src/libstore-tests/store-reference.cc | 60 +++++++++++++++++++ src/libstore/store-reference.cc | 28 ++++++++- 8 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt new file mode 100644 index 000000000..e093c3f30 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_4.txt @@ -0,0 +1 @@ +ssh://userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0]?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt new file mode 100644 index 000000000..8375d3c6d --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_5.txt @@ -0,0 +1 @@ +ssh://userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt new file mode 100644 index 000000000..f5a09c2f7 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_6.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt new file mode 100644 index 000000000..3bef5e73f --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_7.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt new file mode 100644 index 000000000..3db9f9910 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_8.txt @@ -0,0 +1 @@ +ssh://fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt new file mode 100644 index 000000000..ad199cfde --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_9.txt @@ -0,0 +1 @@ +ssh://fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%eth0 \ No newline at end of file diff --git a/src/libstore-tests/store-reference.cc b/src/libstore-tests/store-reference.cc index 7b42b45a2..a52b92b78 100644 --- a/src/libstore-tests/store-reference.cc +++ b/src/libstore-tests/store-reference.cc @@ -183,4 +183,64 @@ static StoreReference sshIPv6AuthorityWithUserinfoAndParams{ URI_TEST_READ(ssh_unbracketed_ipv6_3, sshIPv6AuthorityWithUserinfoAndParams) +static const StoreReference sshIPv6AuthorityWithUserinfoAndParamsAndZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_4, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) +URI_TEST_READ(ssh_unbracketed_ipv6_5, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithUserinfoAndParamsAndZoneIdTricky{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%2525]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +// Non-standard syntax where the IPv6 literal appears without brackets. In +// this case don't considering %25 to be a pct-encoded % and just take it as a +// literal value. 25 is a perfectly legal ZoneId value in theory. +URI_TEST_READ(ssh_unbracketed_ipv6_6, sshIPv6AuthorityWithUserinfoAndParamsAndZoneIdTricky) +URI_TEST_READ(ssh_unbracketed_ipv6_7, sshIPv6AuthorityWithUserinfoAndParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithParamsAndZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_8, sshIPv6AuthorityWithParamsAndZoneId) + +static const StoreReference sshIPv6AuthorityWithZoneId{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e%25eth0]", + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_9, sshIPv6AuthorityWithZoneId) + } // namespace nix diff --git a/src/libstore/store-reference.cc b/src/libstore/store-reference.cc index 0dfc6320c..3bc846ef5 100644 --- a/src/libstore/store-reference.cc +++ b/src/libstore/store-reference.cc @@ -123,7 +123,27 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen * greedily assumed to be the part of the host address. */ auto authorityString = schemeAndAuthority->authority; auto userinfo = splitPrefixTo(authorityString, '@'); - auto maybeIpv6 = boost::urls::parse_ipv6_address(authorityString); + /* Back-compat shim for ZoneId specifiers. Technically this isn't + * standard, but the expectation is this works with the old syntax + * for ZoneID specifiers. For the full story behind the fiasco that + * is ZoneID in URLs look at [^]. + * [^]: https://datatracker.ietf.org/doc/html/draft-schinazi-httpbis-link-local-uri-bcp-03 + */ + + /* Fish out the internals from inside square brackets. It might be that the pct-sign is unencoded and that's + * why we failed to parse it previously. */ + if (authorityString.starts_with('[') && authorityString.ends_with(']')) { + authorityString.remove_prefix(1); + authorityString.remove_suffix(1); + } + + auto maybeBeforePct = splitPrefixTo(authorityString, '%'); + bool hasZoneId = maybeBeforePct.has_value(); + auto maybeZoneId = hasZoneId ? std::optional{authorityString} : std::nullopt; + + std::string_view maybeIpv6S = maybeBeforePct.value_or(authorityString); + auto maybeIpv6 = boost::urls::parse_ipv6_address(maybeIpv6S); + if (maybeIpv6) { std::string fixedAuthority; if (userinfo) { @@ -131,7 +151,11 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen fixedAuthority += '@'; } fixedAuthority += '['; - fixedAuthority += authorityString; + fixedAuthority += maybeIpv6S; + if (maybeZoneId) { + fixedAuthority += "%25"; // pct-encoded percent character + fixedAuthority += *maybeZoneId; + } fixedAuthority += ']'; return { .variant =