bug #27388 [Routing] Account for greediness when merging route patterns (nicolas-grekas)

This PR was merged into the 4.1 branch.

Discussion
----------

[Routing] Account for greediness when merging route patterns

| Q             | A
| ------------- | ---
| Branch?       | 4.1
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #27385
| License       | MIT
| Doc PR        | -

Sub-patterns of variable length should not be considered as common prefixes because their greediness would break in-order matching.

Commits
-------

d5a8237b1c [Routing] Account for greediness when merging route patterns
This commit is contained in:
Nicolas Grekas 2018-05-25 23:27:30 +02:00
commit 9e6fbe8fdb
3 changed files with 47 additions and 29 deletions

View File

@ -151,6 +151,7 @@ class StaticPrefixCollection
$baseLength = \strlen($this->prefix);
$end = min(\strlen($prefix), \strlen($anotherPrefix));
$staticLength = null;
set_error_handler(array(__CLASS__, 'handleError'));
for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) {
if ('(' === $prefix[$i]) {
@ -174,13 +175,24 @@ class StaticPrefixCollection
if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) {
break;
}
$subPattern = substr($prefix, $i, $j - $i);
if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?<!'.$subPattern.')}', '')) {
// sub-patterns of variable length are not considered as common prefixes because their greediness would break in-order matching
break;
}
$i = $j - 1;
} elseif ('\\' === $prefix[$i] && (++$i === $end || $prefix[$i] !== $anotherPrefix[$i])) {
--$i;
break;
}
}
restore_error_handler();
return array(substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i));
}
public static function handleError($type, $msg)
{
return 0 === strpos($msg, 'preg_match(): Compilation failed: lookbehind assertion is not fixed length');
}
}

View File

@ -68,30 +68,26 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
.'|admin/post/(?'
.'|(*:33)'
.'|new(*:43)'
.'|(\\d+)(?'
.'|(*:58)'
.'|/(?'
.'|edit(*:73)'
.'|delete(*:86)'
.')'
.')'
.'|(\\d+)(*:55)'
.'|(\\d+)/edit(*:72)'
.'|(\\d+)/delete(*:91)'
.')'
.'|blog/(?'
.'|(*:104)'
.'|rss\\.xml(*:120)'
.'|(*:107)'
.'|rss\\.xml(*:123)'
.'|p(?'
.'|age/([^/]++)(*:144)'
.'|osts/([^/]++)(*:165)'
.'|age/([^/]++)(*:147)'
.'|osts/([^/]++)(*:168)'
.')'
.'|comments/(\\d+)/new(*:192)'
.'|search(*:206)'
.'|comments/(\\d+)/new(*:195)'
.'|search(*:209)'
.')'
.'|log(?'
.'|in(*:223)'
.'|out(*:234)'
.'|in(*:226)'
.'|out(*:237)'
.')'
.')'
.'|/(en|fr)?(*:253)'
.'|/(en|fr)?(*:256)'
.')$}sD',
);
@ -102,18 +98,18 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
$routes = array(
33 => array(array('_route' => 'a', '_locale' => 'en'), array('_locale'), null, null),
43 => array(array('_route' => 'b', '_locale' => 'en'), array('_locale'), null, null),
58 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
73 => array(array('_route' => 'd', '_locale' => 'en'), array('_locale', 'id'), null, null),
86 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
104 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null),
120 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
144 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
165 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
192 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
206 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
223 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
234 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
253 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
55 => array(array('_route' => 'c', '_locale' => 'en'), array('_locale', 'id'), null, null),
72 => array(array('_route' => 'd', '_locale' => 'en'), array('_locale', 'id'), null, null),
91 => array(array('_route' => 'e', '_locale' => 'en'), array('_locale', 'id'), null, null),
107 => array(array('_route' => 'f', '_locale' => 'en'), array('_locale'), null, null),
123 => array(array('_route' => 'g', '_locale' => 'en'), array('_locale'), null, null),
147 => array(array('_route' => 'h', '_locale' => 'en'), array('_locale', 'page'), null, null),
168 => array(array('_route' => 'i', '_locale' => 'en'), array('_locale', 'page'), null, null),
195 => array(array('_route' => 'j', '_locale' => 'en'), array('_locale', 'id'), null, null),
209 => array(array('_route' => 'k', '_locale' => 'en'), array('_locale'), null, null),
226 => array(array('_route' => 'l', '_locale' => 'en'), array('_locale'), null, null),
237 => array(array('_route' => 'm', '_locale' => 'en'), array('_locale'), null, null),
256 => array(array('_route' => 'n', '_locale' => 'en'), array('_locale'), null, null),
);
list($ret, $vars, $requiredMethods, $requiredSchemes) = $routes[$m];
@ -139,7 +135,7 @@ class ProjectUrlMatcher extends Symfony\Component\Routing\Tests\Fixtures\Redirec
return $ret;
}
if (253 === $m) {
if (256 === $m) {
break;
}
$regex = substr_replace($regex, 'F', $m - $offset, 1 + strlen($m));

View File

@ -611,6 +611,16 @@ class UrlMatcherTest extends TestCase
$this->assertEquals(array('_route' => 'a', 'a' => 'a', 'b' => 'b'), $matcher->match('/a/b'));
}
public function testDotAllWithCatchAll()
{
$coll = new RouteCollection();
$coll->add('a', new Route('/{id}.html', array(), array('id' => '.+')));
$coll->add('b', new Route('/{all}', array(), array('all' => '.+')));
$matcher = $this->getUrlMatcher($coll);
$this->assertEquals(array('_route' => 'a', 'id' => 'foo/bar'), $matcher->match('/foo/bar.html'));
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{
return new UrlMatcher($routes, $context ?: new RequestContext());