From c174e164fc690b6ca13efb3d8346ef20f707197c Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Tue, 11 Aug 2026 17:37:05 -0700 Subject: [PATCH 1/4] feat(gax): implement REST URI percent-encoding and dot segment validation --- .../RelativeResourceTemplate.php | 108 +++++++++++++++--- Gax/tests/Unit/PathTemplateTest.php | 8 +- .../AbsoluteResourceTemplateTest.php | 8 +- .../RelativeResourceTemplateTest.php | 104 ++++++++++++++++- 4 files changed, 202 insertions(+), 26 deletions(-) diff --git a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php index 47ca7dac2622..6c255644e58d 100644 --- a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php +++ b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php @@ -115,21 +115,89 @@ public function render(array $bindings) throw $this->renderingException($bindings, "missing required binding '$key' for segment '$segment'"); } $value = $bindings[$key]; - if (!is_null($value) && $segment->matches($value)) { - $literalSegments[] = new Segment( - Segment::LITERAL_SEGMENT, - $value, - $segment->getValue(), - $segment->getTemplate(), - $segment->getSeparator() - ); - } else { - $valueString = is_null($value) ? 'null' : "'$value'"; - throw $this->renderingException( - $bindings, - "expected binding '$key' to match segment '$segment', instead got $valueString" - ); + if (!is_null($value)) { + $matches = false; + if ($segment->getSegmentType() === Segment::VARIABLE_SEGMENT) { + try { + $wildcardBindings = $segment->getTemplate()->match($value); + $matches = true; + + // Validate wildcard bindings for . and .. + $innerTuples = self::buildKeySegmentTuples($segment->getTemplate()->segments); + foreach ($innerTuples as list($innerKey, $innerSegment)) { + if ($innerKey === null || !isset($wildcardBindings[$innerKey])) { + continue; + } + /** @var Segment $innerSegment */ + $wildcardValue = $wildcardBindings[$innerKey]; + + if ($innerSegment->getSegmentType() === Segment::WILDCARD_SEGMENT) { + if ($wildcardValue === '.' || $wildcardValue === '..') { + throw new \InvalidArgumentException(sprintf( + "Invalid value %s for %s.", + $wildcardValue, + $key + )); + } + } elseif ($innerSegment->getSegmentType() === Segment::DOUBLE_WILDCARD_SEGMENT) { + $parts = explode('/', $wildcardValue); + foreach ($parts as $part) { + if ($part === '.' || $part === '..') { + throw new \InvalidArgumentException(sprintf( + "Value for %s must not contain segments that are exactly . or .. .", + $key + )); + } + } + } + } + } catch (ValidationException $e) { + $matches = false; + } + } else { + $matches = $segment->matches($value); + if ($matches) { + if ($segment->getSegmentType() === Segment::WILDCARD_SEGMENT) { + if ($value === '.' || $value === '..') { + throw new \InvalidArgumentException(sprintf( + "Invalid value %s for %s.", + $value, + $key + )); + } + } elseif ($segment->getSegmentType() === Segment::DOUBLE_WILDCARD_SEGMENT) { + $parts = explode('/', $value); + foreach ($parts as $part) { + if ($part === '.' || $part === '..') { + throw new \InvalidArgumentException(sprintf( + "Value for %s must not contain segments that are exactly . or .. .", + $key + )); + } + } + } + } + } + + if ($matches) { + $encodedValue = self::encodeValue($value); + + $literalSegments[] = new Segment( + Segment::LITERAL_SEGMENT, + $encodedValue, + $segment->getValue(), + $segment->getTemplate(), + $segment->getSeparator() + ); + continue; + } } + + $valueString = is_null($value) ? 'null' : "'$value'"; + throw $this->renderingException( + $bindings, + "expected binding '$key' to match segment '$segment', instead got $valueString" + ); } return self::renderSegments($literalSegments); } @@ -390,4 +458,16 @@ private static function renderSegments(array $segmentsToRender) } return $renderResult; } + + /** + * URL encode the value, while preserving '/' and any characters in [-_.~0-9a-zA-Z]. + * @param string $value + * @return string + */ + private static function encodeValue(string $value) + { + $segments = explode('/', $value); + $encodedSegments = array_map('rawurlencode', $segments); + return implode('/', $encodedSegments); + } } diff --git a/Gax/tests/Unit/PathTemplateTest.php b/Gax/tests/Unit/PathTemplateTest.php index a53c89e1b981..f281f0493f12 100644 --- a/Gax/tests/Unit/PathTemplateTest.php +++ b/Gax/tests/Unit/PathTemplateTest.php @@ -165,7 +165,7 @@ public function testMatchColonInWildcardAndTemplate() $url = $template->render( ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'] ); - $this->assertEquals($url, '/buckets/f/o/o/objects/google.com:a-b:action'); + $this->assertEquals($url, '/buckets/f/o/o/objects/google.com%3Aa-b:action'); } public function testMatchUnboundedWildcardWithColon() @@ -208,7 +208,7 @@ public function testRenderAtomicResource() $url = $template->render( ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'] ); - $this->assertEquals($url, 'buckets/f/o/o/objects/google.com:a-b'); + $this->assertEquals($url, 'buckets/f/o/o/objects/google.com%3Aa-b'); } public function testRenderFailWhenTooFewVariables() @@ -258,7 +258,7 @@ public function testSubstitutionOddChars() ); $this->assertEquals( $url, - 'projects/google.com:proj-test/topics/some-topic' + 'projects/google.com%3Aproj-test/topics/some-topic' ); $template = new PathTemplate('projects/{project}/topics/{topic}'); $url = $template->render( @@ -266,7 +266,7 @@ public function testSubstitutionOddChars() ); $this->assertEquals( $url, - 'projects/g.,;:~`!@#$%^&()+-/topics/sdf<>,.?[]' + 'projects/g.%2C%3B%3A~%60%21%40%23%24%25%5E%26%28%29%2B-/topics/sdf%3C%3E%2C.%3F%5B%5D' ); } } diff --git a/Gax/tests/Unit/ResourceTemplate/AbsoluteResourceTemplateTest.php b/Gax/tests/Unit/ResourceTemplate/AbsoluteResourceTemplateTest.php index 65c84ffa4b99..8c8879042b9d 100644 --- a/Gax/tests/Unit/ResourceTemplate/AbsoluteResourceTemplateTest.php +++ b/Gax/tests/Unit/ResourceTemplate/AbsoluteResourceTemplateTest.php @@ -165,8 +165,8 @@ public function matchData() ], [ '/buckets/*/*/*/objects/*:action', - '/buckets/f/o/o/objects/google.com:a-b:action', - ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'], + '/buckets/f/o/o/objects/google.com-a-b:action', + ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com-a-b'], ], [ '/buckets/*/objects/**:action', @@ -180,8 +180,8 @@ public function matchData() ], [ '/buckets/*', - '/buckets/{}!@#$%^&*()+=[]\|`~-_', - ['$0' => '{}!@#$%^&*()+=[]\|`~-_'], + '/buckets/abc~-_', + ['$0' => 'abc~-_'], ], ]; } diff --git a/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php b/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php index 6e95f663a76c..2b0b95317f65 100644 --- a/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php +++ b/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php @@ -186,8 +186,8 @@ public function matchData() ], [ 'buckets/*/*/*/objects/*', - 'buckets/f/o/o/objects/google.com:a-b', - ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'], + 'buckets/f/o/o/objects/google.com-a-b', + ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com-a-b'], ], [ 'buckets/*/objects/**', @@ -201,8 +201,8 @@ public function matchData() ], [ 'buckets/*', - 'buckets/{}!@#$%^&*()+=[]\|`~-_', - ['$0' => '{}!@#$%^&*()+=[]\|`~-_'], + 'buckets/abc~-_', + ['$0' => 'abc~-_'], ], [ 'foos/{foo}_{oof}', @@ -381,4 +381,100 @@ public function invalidRenderData() ], ]; } + + /** + * @param string $pathTemplate + * @param array $bindings + * @param string $expectedExceptionMessage + * @dataProvider invalidRenderDataInvalidArgument + */ + public function testFailRenderInvalidArgument($pathTemplate, $bindings, $expectedExceptionMessage = null) + { + $this->expectException(\InvalidArgumentException::class); + if (isset($expectedExceptionMessage)) { + $this->expectExceptionMessage($expectedExceptionMessage); + } + + $template = new RelativeResourceTemplate($pathTemplate); + $template->render($bindings); + } + + public function invalidRenderDataInvalidArgument() + { + return [ + [ + 'buckets/{hello}', + ['hello' => '.'], + "Invalid value . for hello.", + ], + [ + 'buckets/{hello}', + ['hello' => '..'], + "Invalid value .. for hello.", + ], + [ + 'buckets/{hello=*}', + ['hello' => '.'], + "Invalid value . for hello.", + ], + [ + 'buckets/{hello=**}', + ['hello' => 'foo/./bar'], + "Value for hello must not contain segments that are exactly . or .. .", + ], + [ + 'buckets/{hello=**}', + ['hello' => 'foo/..'], + "Value for hello must not contain segments that are exactly . or .. .", + ], + [ + 'buckets/*/objects/**', + ['$0' => '.', '$1' => 'foo/bar'], + "Invalid value . for $0.", + ], + [ + 'buckets/*/objects/**', + ['$0' => 'foo', '$1' => '../bar'], + "Value for $1 must not contain segments that are exactly . or .. .", + ], + [ + 'projects/*/locations/*', + ['$0' => 'my-proj', '$1' => '.'], + "Invalid value . for $1.", + ] + ]; + } + + /** + * @dataProvider renderEncodingData + */ + public function testRenderEncoding($pathTemplate, $expectedPath, $bindings) + { + $template = new RelativeResourceTemplate($pathTemplate); + $this->assertEquals($expectedPath, $template->render($bindings)); + } + + public function renderEncodingData() + { + return [ + [ + 'buckets/{hello}', + 'buckets/world%20order', + ['hello' => 'world order'], + ], + [ + 'buckets/{hello=**}', + 'buckets/foo/bar%21/baz~', + ['hello' => 'foo/bar!/baz~'], + ], + [ + 'projects/{project}/locations/{location}', + 'projects/my%20project/locations/us-central1', + [ + 'project' => 'my project', + 'location' => 'us-central1', + ] + ], + ]; + } } From 294d573906e418e61f3d4ebb54fe42c79fcdf1f2 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Wed, 12 Aug 2026 01:06:43 -0700 Subject: [PATCH 2/4] fix(gax): disable resource name url encoding by default --- Gax/src/PathTemplate.php | 4 ++-- Gax/src/RequestBuilder.php | 2 +- .../ResourceTemplate/AbsoluteResourceTemplate.php | 4 ++-- .../ResourceTemplate/RelativeResourceTemplate.php | 4 ++-- Gax/tests/Unit/PathTemplateTest.php | 12 ++++++++---- .../RelativeResourceTemplateTest.php | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Gax/src/PathTemplate.php b/Gax/src/PathTemplate.php index 4556acf077b3..464f32dc1ffb 100644 --- a/Gax/src/PathTemplate.php +++ b/Gax/src/PathTemplate.php @@ -83,9 +83,9 @@ public function __toString() * can't be parsed. * @return string A rendered representation of this path template. */ - public function render(array $bindings) + public function render(array $bindings, bool $urlEncode = false) { - return $this->resourceTemplate->render($bindings); + return $this->resourceTemplate->render($bindings, $urlEncode); } /** diff --git a/Gax/src/RequestBuilder.php b/Gax/src/RequestBuilder.php index 330d13dd4239..24af4c050d04 100644 --- a/Gax/src/RequestBuilder.php +++ b/Gax/src/RequestBuilder.php @@ -259,7 +259,7 @@ private function tryRenderPathTemplate(string $uriTemplate, array $bindings) $template = new AbsoluteResourceTemplate($uriTemplate); try { - return $template->render($bindings); + return $template->render($bindings, true); } catch (ValidationException $e) { return null; } diff --git a/Gax/src/ResourceTemplate/AbsoluteResourceTemplate.php b/Gax/src/ResourceTemplate/AbsoluteResourceTemplate.php index 7f2425f2eaf6..de08c38c7d59 100644 --- a/Gax/src/ResourceTemplate/AbsoluteResourceTemplate.php +++ b/Gax/src/ResourceTemplate/AbsoluteResourceTemplate.php @@ -88,9 +88,9 @@ public function __toString() /** * @inheritdoc */ - public function render(array $bindings) + public function render(array $bindings, bool $urlEncode = false) { - return sprintf('/%s%s', $this->resourceTemplate->render($bindings), $this->renderVerb()); + return sprintf('/%s%s', $this->resourceTemplate->render($bindings, $urlEncode), $this->renderVerb()); } /** diff --git a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php index 6c255644e58d..bcd420503efd 100644 --- a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php +++ b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php @@ -101,7 +101,7 @@ public function __toString() /** * @inheritdoc */ - public function render(array $bindings) + public function render(array $bindings, bool $urlEncode = false) { $literalSegments = []; $keySegmentTuples = self::buildKeySegmentTuples($this->segments); @@ -180,7 +180,7 @@ public function render(array $bindings) } if ($matches) { - $encodedValue = self::encodeValue($value); + $encodedValue = $urlEncode ? self::encodeValue($value) : $value; $literalSegments[] = new Segment( Segment::LITERAL_SEGMENT, diff --git a/Gax/tests/Unit/PathTemplateTest.php b/Gax/tests/Unit/PathTemplateTest.php index f281f0493f12..f994374e4fea 100644 --- a/Gax/tests/Unit/PathTemplateTest.php +++ b/Gax/tests/Unit/PathTemplateTest.php @@ -163,7 +163,8 @@ public function testMatchColonInWildcardAndTemplate() { $template = new PathTemplate('/buckets/*/*/*/objects/*:action'); $url = $template->render( - ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'] + ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'], + true ); $this->assertEquals($url, '/buckets/f/o/o/objects/google.com%3Aa-b:action'); } @@ -206,7 +207,8 @@ public function testRenderAtomicResource() { $template = new PathTemplate('buckets/*/*/*/objects/*'); $url = $template->render( - ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'] + ['$0' => 'f', '$1' => 'o', '$2' => 'o', '$3' => 'google.com:a-b'], + true ); $this->assertEquals($url, 'buckets/f/o/o/objects/google.com%3Aa-b'); } @@ -254,7 +256,8 @@ public function testSubstitutionOddChars() { $template = new PathTemplate('projects/{project}/topics/{topic}'); $url = $template->render( - ['project' => 'google.com:proj-test', 'topic' => 'some-topic'] + ['project' => 'google.com:proj-test', 'topic' => 'some-topic'], + true ); $this->assertEquals( $url, @@ -262,7 +265,8 @@ public function testSubstitutionOddChars() ); $template = new PathTemplate('projects/{project}/topics/{topic}'); $url = $template->render( - ['project' => 'g.,;:~`!@#$%^&()+-', 'topic' => 'sdf<>,.?[]'] + ['project' => 'g.,;:~`!@#$%^&()+-', 'topic' => 'sdf<>,.?[]'], + true ); $this->assertEquals( $url, diff --git a/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php b/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php index 2b0b95317f65..3a84b1fffd2a 100644 --- a/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php +++ b/Gax/tests/Unit/ResourceTemplate/RelativeResourceTemplateTest.php @@ -451,7 +451,7 @@ public function invalidRenderDataInvalidArgument() public function testRenderEncoding($pathTemplate, $expectedPath, $bindings) { $template = new RelativeResourceTemplate($pathTemplate); - $this->assertEquals($expectedPath, $template->render($bindings)); + $this->assertEquals($expectedPath, $template->render($bindings, true)); } public function renderEncodingData() From 96d8e905c2268a40cf8cdfddebd30b51175a2f06 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Wed, 12 Aug 2026 09:43:58 -0700 Subject: [PATCH 3/4] style(gax): fix double quotes to single quotes --- Gax/src/ResourceTemplate/RelativeResourceTemplate.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php index bcd420503efd..fccf89ba963b 100644 --- a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php +++ b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php @@ -134,7 +134,7 @@ public function render(array $bindings, bool $urlEncode = false) if ($innerSegment->getSegmentType() === Segment::WILDCARD_SEGMENT) { if ($wildcardValue === '.' || $wildcardValue === '..') { throw new \InvalidArgumentException(sprintf( - "Invalid value %s for %s.", + 'Invalid value %s for %s.', $wildcardValue, $key )); @@ -144,7 +144,7 @@ public function render(array $bindings, bool $urlEncode = false) foreach ($parts as $part) { if ($part === '.' || $part === '..') { throw new \InvalidArgumentException(sprintf( - "Value for %s must not contain segments that are exactly . or .. .", + 'Value for %s must not contain segments that are exactly . or .. .', $key )); } @@ -160,7 +160,7 @@ public function render(array $bindings, bool $urlEncode = false) if ($segment->getSegmentType() === Segment::WILDCARD_SEGMENT) { if ($value === '.' || $value === '..') { throw new \InvalidArgumentException(sprintf( - "Invalid value %s for %s.", + 'Invalid value %s for %s.', $value, $key )); @@ -170,7 +170,7 @@ public function render(array $bindings, bool $urlEncode = false) foreach ($parts as $part) { if ($part === '.' || $part === '..') { throw new \InvalidArgumentException(sprintf( - "Value for %s must not contain segments that are exactly . or .. .", + 'Value for %s must not contain segments that are exactly . or .. .', $key )); } From 5ad1663381ef8f69ed556b78cdd7ed6250445a28 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Wed, 12 Aug 2026 17:51:56 -0700 Subject: [PATCH 4/4] refactor(gax): extract segment validation logic --- .../RelativeResourceTemplate.php | 148 +++++++++--------- 1 file changed, 70 insertions(+), 78 deletions(-) diff --git a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php index fccf89ba963b..cefc3b8acf22 100644 --- a/Gax/src/ResourceTemplate/RelativeResourceTemplate.php +++ b/Gax/src/ResourceTemplate/RelativeResourceTemplate.php @@ -115,91 +115,83 @@ public function render(array $bindings, bool $urlEncode = false) throw $this->renderingException($bindings, "missing required binding '$key' for segment '$segment'"); } $value = $bindings[$key]; - if (!is_null($value)) { - $matches = false; - if ($segment->getSegmentType() === Segment::VARIABLE_SEGMENT) { - try { - $wildcardBindings = $segment->getTemplate()->match($value); - $matches = true; - - // Validate wildcard bindings for . and .. - $innerTuples = self::buildKeySegmentTuples($segment->getTemplate()->segments); - foreach ($innerTuples as list($innerKey, $innerSegment)) { - if ($innerKey === null || !isset($wildcardBindings[$innerKey])) { - continue; - } - /** @var Segment $innerSegment */ - $wildcardValue = $wildcardBindings[$innerKey]; - - if ($innerSegment->getSegmentType() === Segment::WILDCARD_SEGMENT) { - if ($wildcardValue === '.' || $wildcardValue === '..') { - throw new \InvalidArgumentException(sprintf( - 'Invalid value %s for %s.', - $wildcardValue, - $key - )); - } - } elseif ($innerSegment->getSegmentType() === Segment::DOUBLE_WILDCARD_SEGMENT) { - $parts = explode('/', $wildcardValue); - foreach ($parts as $part) { - if ($part === '.' || $part === '..') { - throw new \InvalidArgumentException(sprintf( - 'Value for %s must not contain segments that are exactly . or .. .', - $key - )); - } - } - } - } - } catch (ValidationException $e) { - $matches = false; - } - } else { - $matches = $segment->matches($value); - if ($matches) { - if ($segment->getSegmentType() === Segment::WILDCARD_SEGMENT) { - if ($value === '.' || $value === '..') { - throw new \InvalidArgumentException(sprintf( - 'Invalid value %s for %s.', - $value, - $key - )); - } - } elseif ($segment->getSegmentType() === Segment::DOUBLE_WILDCARD_SEGMENT) { - $parts = explode('/', $value); - foreach ($parts as $part) { - if ($part === '.' || $part === '..') { - throw new \InvalidArgumentException(sprintf( - 'Value for %s must not contain segments that are exactly . or .. .', - $key - )); - } - } - } + if (is_null($value)) { + throw $this->renderingException( + $bindings, + "expected binding '$key' to match segment '$segment', instead got null" + ); + } + + if (!$this->matchAndValidateSegment($segment, (string)$value, (string)$key)) { + throw $this->renderingException( + $bindings, + "expected binding '$key' to match segment '$segment', instead got '$value'" + ); + } + + $encodedValue = $urlEncode ? self::encodeValue($value) : $value; + $literalSegments[] = new Segment( + Segment::LITERAL_SEGMENT, + $encodedValue, + $segment->getValue(), + $segment->getTemplate(), + $segment->getSeparator() + ); + } + return self::renderSegments($literalSegments); + } + + private function matchAndValidateSegment(Segment $segment, string $value, string $key): bool + { + if ($segment->getSegmentType() === Segment::VARIABLE_SEGMENT) { + try { + $wildcardBindings = $segment->getTemplate()->match($value); + + // Validate wildcard bindings for . and .. + $innerTuples = self::buildKeySegmentTuples($segment->getTemplate()->segments); + foreach ($innerTuples as list($innerKey, $innerSegment)) { + if ($innerKey === null || !isset($wildcardBindings[$innerKey])) { + continue; } + /** @var Segment $innerSegment */ + $wildcardValue = $wildcardBindings[$innerKey]; + self::validateDotSegments($innerSegment->getSegmentType(), $wildcardValue, $key); } + return true; + } catch (ValidationException $e) { + return false; + } + } - if ($matches) { - $encodedValue = $urlEncode ? self::encodeValue($value) : $value; + $matches = $segment->matches($value); + if ($matches) { + self::validateDotSegments($segment->getSegmentType(), $value, $key); + } + + return $matches; + } - $literalSegments[] = new Segment( - Segment::LITERAL_SEGMENT, - $encodedValue, - $segment->getValue(), - $segment->getTemplate(), - $segment->getSeparator() - ); - continue; + private static function validateDotSegments(int $segmentType, string $value, string $key): void + { + if ($segmentType === Segment::WILDCARD_SEGMENT) { + if ($value === '.' || $value === '..') { + throw new \InvalidArgumentException(sprintf( + 'Invalid value %s for %s.', + $value, + $key + )); + } + } elseif ($segmentType === Segment::DOUBLE_WILDCARD_SEGMENT) { + $parts = explode('/', $value); + foreach ($parts as $part) { + if ($part === '.' || $part === '..') { + throw new \InvalidArgumentException(sprintf( + 'Value for %s must not contain segments that are exactly . or .. .', + $key + )); } } - - $valueString = is_null($value) ? 'null' : "'$value'"; - throw $this->renderingException( - $bindings, - "expected binding '$key' to match segment '$segment', instead got $valueString" - ); } - return self::renderSegments($literalSegments); } /**