Skip to content

Commit f907dc6

Browse files
ktomkSpacePossum
authored andcommitted
More details on PCRE (pattern) errors.
1 parent 1bb9417 commit f907dc6

File tree

2 files changed

+158
-11
lines changed

2 files changed

+158
-11
lines changed

src/Preg.php

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public static function match($pattern, $subject, &$matches = null, $flags = 0, $
4545
return $result;
4646
}
4747

48-
throw new PregException('Error occurred when calling preg_match.', preg_last_error());
48+
throw self::newPregException(preg_last_error(), __METHOD__, (array) $pattern);
4949
}
5050

5151
/**
@@ -71,7 +71,7 @@ public static function matchAll($pattern, $subject, &$matches = null, $flags = P
7171
return $result;
7272
}
7373

74-
throw new PregException('Error occurred when calling preg_match_all.', preg_last_error());
74+
throw self::newPregException(preg_last_error(), __METHOD__, (array) $pattern);
7575
}
7676

7777
/**
@@ -97,7 +97,7 @@ public static function replace($pattern, $replacement, $subject, $limit = -1, &$
9797
return $result;
9898
}
9999

100-
throw new PregException('Error occurred when calling preg_replace.', preg_last_error());
100+
throw self::newPregException(preg_last_error(), __METHOD__, (array) $pattern);
101101
}
102102

103103
/**
@@ -123,7 +123,7 @@ public static function replaceCallback($pattern, $callback, $subject, $limit = -
123123
return $result;
124124
}
125125

126-
throw new PregException('Error occurred when calling preg_replace_callback.', preg_last_error());
126+
throw self::newPregException(preg_last_error(), __METHOD__, (array) $pattern);
127127
}
128128

129129
/**
@@ -148,7 +148,7 @@ public static function split($pattern, $subject, $limit = -1, $flags = 0)
148148
return $result;
149149
}
150150

151-
throw new PregException('Error occurred when calling preg_split.', preg_last_error());
151+
throw self::newPregException(preg_last_error(), __METHOD__, (array) $pattern);
152152
}
153153

154154
/**
@@ -180,10 +180,54 @@ private static function removeUtf8Modifier($pattern)
180180
return '';
181181
}
182182

183-
$delimiter = substr($pattern, 0, 1);
183+
$delimiter = $pattern[0];
184184

185185
$endDelimiterPosition = strrpos($pattern, $delimiter);
186186

187187
return substr($pattern, 0, $endDelimiterPosition).str_replace('u', '', substr($pattern, $endDelimiterPosition));
188188
}
189+
190+
/**
191+
* Create PregException.
192+
*
193+
* Create the generic PregException message and if possible due to finding
194+
* an invalid pattern, tell more about such kind of error in the message.
195+
*
196+
* @param int $error
197+
* @param string $method
198+
* @param string[] $patterns
199+
*
200+
* @return PregException
201+
*/
202+
private static function newPregException($error, $method, array $patterns)
203+
{
204+
foreach ($patterns as $pattern) {
205+
$last = error_get_last();
206+
$result = @preg_match($pattern, '');
207+
208+
if (false !== $result) {
209+
continue;
210+
}
211+
212+
$code = preg_last_error();
213+
$next = error_get_last();
214+
215+
if ($last !== $next) {
216+
$message = sprintf(
217+
'(code: %d) %s',
218+
$code,
219+
preg_replace('~preg_[a-z_]+[()]{2}: ~', '', $next['message'])
220+
);
221+
} else {
222+
$message = sprintf('(code: %d)', $code);
223+
}
224+
225+
return new PregException(
226+
sprintf('%s(): Invalid PCRE pattern "%s": %s (version: %s)', $method, $pattern, $message, PCRE_VERSION),
227+
$code
228+
);
229+
}
230+
231+
return new PregException(sprintf('Error occurred when calling %s.', $method), $error);
232+
}
189233
}

tests/PregTest.php

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class PregTest extends TestCase
2727
public function testMatchFailing()
2828
{
2929
$this->expectException(PregException::class);
30-
$this->expectExceptionMessage('Error occurred when calling preg_match.');
30+
$this->expectExceptionMessage('Preg::match(): Invalid PCRE pattern ""');
3131

3232
Preg::match('', 'foo', $matches);
3333
}
@@ -47,10 +47,113 @@ public function testMatch($pattern, $subject)
4747
static::assertSame($expectedMatches, $actualMatches);
4848
}
4949

50+
public function providePatternValidationCases()
51+
{
52+
return [
53+
'invalid_blank' => ['', null, PregException::class],
54+
'invalid_open' => ["\1", null, PregException::class, "'\1' found"],
55+
'valid_control_character_delimiter' => ["\1\1", 1],
56+
'invalid_control_character_modifier' => ["\1\1\1", null, PregException::class, ' Unknown modifier '],
57+
'valid_slate' => ['//', 1],
58+
'valid_paired' => ['()', 1],
59+
'null_byte_injection' => ['()'."\0", null, PregException::class, ' Null byte in regex '],
60+
'paired_non_utf8_only' => ["((*UTF8)\xFF)", null, PregException::class, 'UTF-8'],
61+
'valid_paired_non_utf8_only' => ["(\xFF)", 1],
62+
'php_version_dependent' => ['([\\R])', 0, PregException::class, 'Compilation failed: escape sequence is invalid '],
63+
];
64+
}
65+
66+
/**
67+
* @dataProvider providePatternValidationCases
68+
*
69+
* @param $pattern
70+
* @param null|int $expected
71+
* @param null|string $expectedException
72+
* @param null|string $expectedMessage
73+
*/
74+
public function testPatternValidation($pattern, $expected = null, $expectedException = null, $expectedMessage = null)
75+
{
76+
$setup = function () use ($expectedException, $expectedMessage) {
77+
$i = 0;
78+
79+
if (null !== $expectedException) {
80+
++$i;
81+
$this->expectException($expectedException);
82+
}
83+
84+
if (null !== $expectedMessage) {
85+
++$i;
86+
$this->expectExceptionMessage($expectedMessage);
87+
}
88+
89+
return (bool) $i;
90+
};
91+
92+
try {
93+
$actual = Preg::match($pattern, "The quick brown \xFF\x00\\xXX jumps over the lazy dog\n");
94+
} catch (\Exception $ex) {
95+
$setup();
96+
97+
throw $ex;
98+
}
99+
100+
if (null !== $expected) {
101+
static::assertSame($expected, $actual);
102+
103+
return;
104+
}
105+
106+
$setup() || $this->addToAssertionCount(1);
107+
}
108+
109+
/**
110+
* @dataProvider providePatternValidationCases
111+
*
112+
* @param string $pattern
113+
* @param null|int $expected
114+
* @param null|string $expectedException
115+
* @param null|string $expectedMessage
116+
*/
117+
public function testPatternsValidation($pattern, $expected = null, $expectedException = null, $expectedMessage = null)
118+
{
119+
$setup = function () use ($expectedException, $expectedMessage) {
120+
$i = 0;
121+
122+
if (null !== $expectedException) {
123+
++$i;
124+
$this->expectException($expectedException);
125+
}
126+
127+
if (null !== $expectedMessage) {
128+
++$i;
129+
$this->expectExceptionMessage($expectedMessage);
130+
}
131+
132+
return (bool) $i;
133+
};
134+
135+
try {
136+
$buffer = "The quick brown \xFF\x00\\xXX jumps over the lazy dog\n";
137+
$actual = $buffer !== Preg::replace((array) $pattern, 'abc', $buffer);
138+
} catch (\Exception $ex) {
139+
$setup();
140+
141+
throw $ex;
142+
}
143+
144+
if (null !== $expected) {
145+
static::assertSame((bool) $expected, $actual);
146+
147+
return;
148+
}
149+
150+
$setup() || $this->addToAssertionCount(1);
151+
}
152+
50153
public function testMatchAllFailing()
51154
{
52155
$this->expectException(PregException::class);
53-
$this->expectExceptionMessage('Error occurred when calling preg_match_all.');
156+
$this->expectExceptionMessage('Preg::matchAll(): Invalid PCRE pattern ""');
54157

55158
Preg::matchAll('', 'foo', $matches);
56159
}
@@ -73,7 +176,7 @@ public function testMatchAll($pattern, $subject)
73176
public function testReplaceFailing()
74177
{
75178
$this->expectException(PregException::class);
76-
$this->expectExceptionMessage('Error occurred when calling preg_replace.');
179+
$this->expectExceptionMessageRegExp('~\Q\Preg::replace()\E: Invalid PCRE pattern "": \(code: \d+\) [^(]+ \(version: \d+~');
77180

78181
Preg::replace('', 'foo', 'bar');
79182
}
@@ -96,7 +199,7 @@ public function testReplace($pattern, $subject)
96199
public function testReplaceCallbackFailing()
97200
{
98201
$this->expectException(PregException::class);
99-
$this->expectExceptionMessage('Error occurred when calling preg_replace_callback.');
202+
$this->expectExceptionMessage('Preg::replaceCallback(): Invalid PCRE pattern ""');
100203

101204
Preg::replaceCallback('', 'sort', 'foo');
102205
}
@@ -140,7 +243,7 @@ public function provideArrayOfPatternsCases()
140243
public function testSplitFailing()
141244
{
142245
$this->expectException(PregException::class);
143-
$this->expectExceptionMessage('Error occurred when calling preg_split.');
246+
$this->expectExceptionMessage('Preg::split(): Invalid PCRE pattern ""');
144247

145248
Preg::split('', 'foo');
146249
}

0 commit comments

Comments
 (0)