From 7413c5e273dde8285f414948051cea7dab2e8484 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 21 Jun 2026 14:09:01 +0200 Subject: [PATCH] fix uri parsing idempotency --- CHANGELOG.md | 4 ++++ src/Authority/UserInformation/Password.php | 9 +++++++-- src/Authority/UserInformation/User.php | 9 +++++++-- src/Fragment.php | 9 +++++++-- src/Query.php | 9 +++++++-- src/Scheme.php | 2 +- tests/UrlTest.php | 8 ++++++++ 7 files changed, 41 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77cbea9..5122ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - _Scheme less_ urls now keep their leading `//` when converted to strings +### Fixed + +- Url parsing idempotency with partially encoded characters + ## 5.3.0 - 2026-05-14 ### Changed diff --git a/src/Authority/UserInformation/Password.php b/src/Authority/UserInformation/Password.php index eeaa224..f8f7eaf 100644 --- a/src/Authority/UserInformation/Password.php +++ b/src/Authority/UserInformation/Password.php @@ -54,8 +54,13 @@ public static function none(): self */ public static function parsed(Uri|Concrete $parsed): self { - /** @psalm-suppress ImpureMethodCall */ - $password = $parsed->getPassword(); + if ($parsed instanceof Uri) { + /** @psalm-suppress ImpureMethodCall */ + $password = $parsed->getRawPassword(); + } else { + /** @psalm-suppress ImpureMethodCall */ + $password = $parsed->getPassword(); + } return match ($password) { null => self::none(), diff --git a/src/Authority/UserInformation/User.php b/src/Authority/UserInformation/User.php index 0f82dda..eae152f 100644 --- a/src/Authority/UserInformation/User.php +++ b/src/Authority/UserInformation/User.php @@ -53,8 +53,13 @@ public static function none(): self */ public static function parsed(Uri|Concrete $parsed): self { - /** @psalm-suppress ImpureMethodCall */ - $user = $parsed->getUsername(); + if ($parsed instanceof Uri) { + /** @psalm-suppress ImpureMethodCall */ + $user = $parsed->getRawUsername(); + } else { + /** @psalm-suppress ImpureMethodCall */ + $user = $parsed->getUsername(); + } return match ($user) { null => self::none(), diff --git a/src/Fragment.php b/src/Fragment.php index 8041abc..13e98d1 100644 --- a/src/Fragment.php +++ b/src/Fragment.php @@ -43,8 +43,13 @@ public static function none(): self */ public static function parsed(Uri|Concrete $parsed): self { - /** @psalm-suppress ImpureMethodCall */ - $fragment = $parsed->getFragment(); + if ($parsed instanceof Uri) { + /** @psalm-suppress ImpureMethodCall */ + $fragment = $parsed->getRawFragment(); + } else { + /** @psalm-suppress ImpureMethodCall */ + $fragment = $parsed->getFragment(); + } return match ($fragment) { null => self::none(), diff --git a/src/Query.php b/src/Query.php index 92f4b69..e2f4ff2 100644 --- a/src/Query.php +++ b/src/Query.php @@ -43,8 +43,13 @@ public static function none(): self */ public static function parsed(Uri|Concrete $parsed): self { - /** @psalm-suppress ImpureMethodCall */ - $query = $parsed->getQuery(); + if ($parsed instanceof Uri) { + /** @psalm-suppress ImpureMethodCall */ + $query = $parsed->getRawQuery(); + } else { + /** @psalm-suppress ImpureMethodCall */ + $query = $parsed->getQuery(); + } return match ($query) { null => self::none(), diff --git a/src/Scheme.php b/src/Scheme.php index 4e0f795..23f3515 100644 --- a/src/Scheme.php +++ b/src/Scheme.php @@ -43,7 +43,7 @@ public static function parsed( #[\SensitiveParameter] string $origin, ): self { /** @psalm-suppress ImpureMethodCall */ - $scheme = $parsed->getScheme(); + $scheme = $parsed->getRawScheme(); return match ($scheme) { null => match (\str_starts_with($origin, '//')) { diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 6b5d932..a730cac 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -339,6 +339,14 @@ public function testSchemeLessUrls(): BlackBox\Proof }); } + public function testParsingIdempotencyOfPartiallyEncodedUrls() + { + $this->assertSame( + 'http://h.tld/a%20b?x=%2e', + Url::of(Url::of('http://h.tld/a b?x=%2e')->toString())->toString(), + ); + } + public static function cases(): array { return [