diff --git a/src/JsonSchema/Uri/UriResolver.php b/src/JsonSchema/Uri/UriResolver.php index 00a47e81..267d8fd9 100644 --- a/src/JsonSchema/Uri/UriResolver.php +++ b/src/JsonSchema/Uri/UriResolver.php @@ -144,14 +144,12 @@ public static function combineRelativePathWithBasePath($relativePath, $basePath) while ($combinedSegments) { $segment = array_shift($combinedSegments); if ($segment === '..') { - if (count($collapsedSegments) <= 1) { - // Do not remove the top level (domain) - // This is not ideal - the domain should not be part of the path here. parse() and generate() - // should handle the "domain" separately, like the schema. - // Then the if-condition here would be `if (!$collapsedSegments) {`. - throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); + // RFC 3986 section 5.2.4: a parent segment climbing past the root is + // discarded rather than being an error. The leading empty segment of an + // absolute path is the root itself, so it is never popped. + if ([] !== $collapsedSegments && [''] !== $collapsedSegments) { + array_pop($collapsedSegments); } - array_pop($collapsedSegments); } else { $collapsedSegments[] = $segment; } diff --git a/tests/Uri/UriResolverTest.php b/tests/Uri/UriResolverTest.php index 91dee39f..f17a4748 100644 --- a/tests/Uri/UriResolverTest.php +++ b/tests/Uri/UriResolverTest.php @@ -5,6 +5,7 @@ namespace JsonSchema\Tests\Uri; use JsonSchema\Uri\UriResolver; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class UriResolverTest extends TestCase @@ -105,6 +106,37 @@ public function testCombineRelativePathWithBasePathTraversingUp(): void ); } + /** + * @dataProvider excessParentSegmentCases + */ + #[DataProvider('excessParentSegmentCases')] + public function testCombineRelativePathWithBasePathDiscardsExcessParentSegments(string $expected, string $relativePath, string $basePath): void + { + $this->assertEquals($expected, UriResolver::combineRelativePathWithBasePath($relativePath, $basePath)); + } + + /** + * @return array + */ + public static function excessParentSegmentCases(): array + { + return [ + // RFC 3986 section 5.2.4: a parent segment climbing past the root is discarded + 'more parents than segments' => ['/bar.json', '../../../bar.json', '/a/'], + 'parent against the root itself' => ['/bar.json', '../bar.json', '/'], + // the root is never popped, but a real leading segment still is + 'parent against a relative base' => ['bar.json', '../bar.json', 'foo/baz.json'], + ]; + } + + public function testResolveDiscardsExcessParentSegments(): void + { + $this->assertEquals( + 'http://example.org/bar.json', + $this->resolver->resolve('../../../bar.json', 'http://example.org/a/') + ); + } + public function testResolveAbsoluteUri(): void { $this->assertEquals( diff --git a/tests/Uri/UriRetrieverTest.php b/tests/Uri/UriRetrieverTest.php index cf37d77d..3eef5710 100644 --- a/tests/Uri/UriRetrieverTest.php +++ b/tests/Uri/UriRetrieverTest.php @@ -8,7 +8,6 @@ use JsonSchema\Exception\InvalidSchemaMediaTypeException; use JsonSchema\Exception\JsonDecodingException; use JsonSchema\Exception\ResourceNotFoundException; -use JsonSchema\Exception\UriResolverException; use JsonSchema\Uri\UriRetriever; use JsonSchema\Validator; use PHPUnit\Framework\TestCase; @@ -220,9 +219,11 @@ public function testResolveExcessLevelUp(): void { $retriever = new UriRetriever(); - $this->expectException(UriResolverException::class); - $retriever->resolve( - '../schema.json#', 'http://example.org/schema.json#' + // RFC 3986 section 5.2.4: the parent segment climbs past the root, so it is + // discarded instead of making the reference unresolvable + $this->assertEquals( + 'http://example.org/schema.json', + $retriever->resolve('../schema.json#', 'http://example.org/schema.json#') ); }