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 [