From c954bbb7347039e956c37c08801e29916aae5055 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 21 Jun 2026 17:05:12 +0200 Subject: [PATCH 1/4] only generate urls that can be parsed back --- fixtures/Authority.php | 6 ++++- fixtures/Path.php | 27 ++++++++++++++++++- fixtures/Url.php | 61 +++++++++++++++++++++++++++++++++++++++--- tests/UrlTest.php | 11 ++++++++ 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/fixtures/Authority.php b/fixtures/Authority.php index 3302a67..64687e8 100644 --- a/fixtures/Authority.php +++ b/fixtures/Authority.php @@ -24,6 +24,10 @@ public static function any(): Set Authority\Port::any(), Set::of(Model\Port::none()), ), - ); + ) + ->exclude( + static fn($authority) => !$authority->userInformation()->equals(Model\UserInformation::none()) && + $authority->host()->equals(Model\Host::none()), + ); } } diff --git a/fixtures/Path.php b/fixtures/Path.php index 976d89c..eac7e9b 100644 --- a/fixtures/Path.php +++ b/fixtures/Path.php @@ -13,7 +13,32 @@ final class Path */ public static function any(): Set { - return self::strings()->map(Model::of(...)); + return Set::either( + self::relative(), + self::absolute(), + ); + } + + /** + * @return Set + */ + public static function relative(): Set + { + return self::strings() + ->map(static fn($value) => \ltrim($value, '/')) + ->exclude(static fn($value) => \str_ends_with($value, '\\')) + ->map(Model::of(...)); + } + + /** + * @return Set + */ + public static function absolute(): Set + { + return self::strings() + ->map(static fn($value) => '/'.\ltrim($value, '/')) + ->exclude(static fn($value) => \str_ends_with($value, '\\')) + ->map(Model::of(...)); } /** diff --git a/fixtures/Url.php b/fixtures/Url.php index c563c09..e7ae50f 100644 --- a/fixtures/Url.php +++ b/fixtures/Url.php @@ -5,6 +5,7 @@ use Innmind\Url\{ Url as Model, + Scheme as SchemeModel, Authority as AuthorityModel, Path as PathModel, Query as QueryModel, @@ -19,15 +20,21 @@ final class Url */ public static function any(): Set { - return Set::compose( + $url = Set::compose( Model::from(...), - Scheme::any(), + Set::either( + Scheme::any(), + Set::of( + SchemeModel::none(), + SchemeModel::less(), + ), + ), Set::either( Authority::any(), Set::of(AuthorityModel::none()), ), Set::either( - Path::any(), + Path::absolute(), Set::of(PathModel::none()), ), Set::either( @@ -38,6 +45,54 @@ public static function any(): Set Fragment::any(), Set::of(FragmentModel::none()), ), + ) + ->exclude( + static fn($url) => !$url->authority()->port()->equals(AuthorityModel\Port::none()) && ( + $url->authority()->host()->equals(AuthorityModel\Host::none()) || + $url->scheme()->equals(SchemeModel::none()) + ), + ) + ->exclude( + static fn($url) => !$url->query()->equals(QueryModel::none()) && + $url->path()->equals(PathModel::none()), + ) + ->exclude( + static fn($url) => !$url->fragment()->equals(FragmentModel::none()) && + $url->path()->equals(PathModel::none()), + ) + ->exclude( + static fn($url) => $url->scheme()->equals(SchemeModel::less()) && + $url->authority()->equals(AuthorityModel::none()), + ) + ->exclude( + static fn($url) => $url->authority()->equals(AuthorityModel::none()) && !( + $url->scheme()->equals(SchemeModel::none()) || + $url->scheme()->equals(SchemeModel::less()) + ), + ) + ->exclude( + static fn($url) => !$url->scheme()->equals(SchemeModel::none()) && + $url->authority()->port()->value() > 65535, + ); + $path = Path::relative()->map( + static fn($path) => Model::from( + SchemeModel::none(), + AuthorityModel::none(), + $path, + QueryModel::none(), + FragmentModel::none(), + ), ); + $file = Path::absolute()->map( + static fn($path) => Model::from( + SchemeModel::of('file'), + AuthorityModel::none(), + $path, + QueryModel::none(), + FragmentModel::none(), + ), + ); + + return Set::either($url, $path, $file); } } diff --git a/tests/UrlTest.php b/tests/UrlTest.php index a730cac..6378ed0 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -347,6 +347,17 @@ public function testParsingIdempotencyOfPartiallyEncodedUrls() ); } + public function testAnyUrlFixtureCanBeParsed(): BlackBox\Proof + { + return $this + ->forAll(Fixture::any()) + ->prove(function($url) { + $this->assert()->not()->throws( + static fn() => Url::of($url->toString()), + ); + }); + } + public static function cases(): array { return [ From d9a5fc60b5a606e19dc24b895fc1e39c82996fbf Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 21 Jun 2026 17:13:32 +0200 Subject: [PATCH 2/4] make sure any url fixture can be resolved to another --- tests/UrlTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/UrlTest.php b/tests/UrlTest.php index 6378ed0..c65e4d8 100644 --- a/tests/UrlTest.php +++ b/tests/UrlTest.php @@ -358,6 +358,17 @@ public function testAnyUrlFixtureCanBeParsed(): BlackBox\Proof }); } + public function testAnyUrlsCanBeResolved(): BlackBox\Proof + { + return $this + ->forAll(Fixture::any(), Fixture::any()) + ->prove(function($a, $b) { + $this->assert()->not()->throws( + static fn() => $a->resolve($b)->unwrap(), + ); + }); + } + public static function cases(): array { return [ From d0448833e96b520c9a3e0c7f7634233799223ee1 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 21 Jun 2026 17:20:38 +0200 Subject: [PATCH 3/4] exclude escaped @ from paths --- fixtures/Path.php | 1 + 1 file changed, 1 insertion(+) diff --git a/fixtures/Path.php b/fixtures/Path.php index eac7e9b..5811adf 100644 --- a/fixtures/Path.php +++ b/fixtures/Path.php @@ -71,6 +71,7 @@ private static function strings(): Set ) ->filter(static fn($value) => (bool) \preg_match('~\S+~', $value)) ->exclude(static fn($value) => \str_contains($value, '//')) + ->exclude(static fn($value) => \str_contains($value, '\\@')) ->exclude(static fn($value) => \str_starts_with($value, '\\')) ->map(static fn($value) => \trim($value, ' ')); } From 562aadace1f068d3f3e87634ae7bf3c92c3d3789 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 21 Jun 2026 17:34:59 +0200 Subject: [PATCH 4/4] as last resort rely on url parsing to make sure the generated path is a valid one --- fixtures/Path.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fixtures/Path.php b/fixtures/Path.php index 5811adf..59b481c 100644 --- a/fixtures/Path.php +++ b/fixtures/Path.php @@ -3,7 +3,10 @@ namespace Fixtures\Innmind\Url; -use Innmind\Url\Path as Model; +use Innmind\Url\{ + Path as Model, + Url, +}; use Innmind\BlackBox\Set; final class Path @@ -73,6 +76,11 @@ private static function strings(): Set ->exclude(static fn($value) => \str_contains($value, '//')) ->exclude(static fn($value) => \str_contains($value, '\\@')) ->exclude(static fn($value) => \str_starts_with($value, '\\')) - ->map(static fn($value) => \trim($value, ' ')); + ->map(static fn($value) => \trim($value, ' ')) + ->exclude(static fn($value) => $value === '') + ->filter(static fn($value) => Url::attempt($value)->match( + static fn() => true, + static fn() => false, + )); } }