Skip to content

Commit 09baae0

Browse files
authored
[TASK] Remove thecodingmachine/safe dependency (part 3) (#1616)
1 parent 5bf0ddf commit 09baae0

3 files changed

Lines changed: 165 additions & 11 deletions

File tree

Build/phpstan/phpstan-baseline.neon

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ parameters:
1818
count: 1
1919
path: ../../src/CSSList/CSSList.php
2020

21+
-
22+
message: '#^Function iconv is unsafe to use\. It can return FALSE instead of throwing an exception\. Please add ''use function Safe\\iconv;'' at the beginning of the file to use the variant provided by the ''thecodingmachine/safe'' library\.$#'
23+
identifier: theCodingMachineSafe.function
24+
count: 1
25+
path: ../../src/Parsing/ParserState.php
26+
27+
-
28+
message: '#^Function preg_match is unsafe to use\. It can return FALSE instead of throwing an exception\. Please add ''use function Safe\\preg_match;'' at the beginning of the file to use the variant provided by the ''thecodingmachine/safe'' library\.$#'
29+
identifier: theCodingMachineSafe.function
30+
count: 5
31+
path: ../../src/Parsing/ParserState.php
32+
33+
-
34+
message: '#^Function preg_split is unsafe to use\. It can return FALSE instead of throwing an exception\. Please add ''use function Safe\\preg_split;'' at the beginning of the file to use the variant provided by the ''thecodingmachine/safe'' library\.$#'
35+
identifier: theCodingMachineSafe.function
36+
count: 1
37+
path: ../../src/Parsing/ParserState.php
38+
2139
-
2240
message: '#^Negated boolean expression is always true\.$#'
2341
identifier: booleanNot.alwaysTrue

src/Parsing/ParserState.php

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
use Sabberworm\CSS\Comment\Comment;
88
use Sabberworm\CSS\Settings;
99

10-
use function Safe\iconv;
11-
use function Safe\preg_match;
12-
use function Safe\preg_split;
13-
1410
/**
1511
* @internal since 8.7.0
1612
*/
@@ -120,7 +116,11 @@ public function parseIdentifier(bool $ignoreCase = true): string
120116
throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
121117
}
122118
while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
123-
if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
119+
$matchResult = \preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character);
120+
if (!\is_int($matchResult)) {
121+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112617);
122+
}
123+
if ($matchResult !== 0) {
124124
$result .= $character;
125125
} else {
126126
$result .= '\\' . $character;
@@ -144,13 +144,21 @@ public function parseCharacter(bool $isForIdentifier): ?string
144144
if ($this->comes('\\n') || $this->comes('\\r')) {
145145
return '';
146146
}
147-
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
147+
$matchResult = \preg_match('/[0-9a-fA-F]/Su', $this->peek());
148+
if (!\is_int($matchResult)) {
149+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112618);
150+
}
151+
if ($matchResult === 0) {
148152
return $this->consume(1);
149153
}
150154
$hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
151155
if ($this->strlen($hexCodePoint) < 6) {
152156
// Consume whitespace after incomplete unicode escape
153-
if (preg_match('/\\s/isSu', $this->peek()) !== 0) {
157+
$matchResult = \preg_match('/\\s/isSu', $this->peek());
158+
if (!\is_int($matchResult)) {
159+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112619);
160+
}
161+
if ($matchResult !== 0) {
154162
if ($this->comes('\\r\\n')) {
155163
$this->consume(2);
156164
} else {
@@ -164,7 +172,15 @@ public function parseCharacter(bool $isForIdentifier): ?string
164172
$utf32EncodedCharacter .= \chr($codePoint & 0xff);
165173
$codePoint = $codePoint >> 8;
166174
}
167-
return iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
175+
// The suppression is needed because `\iconv` emits a notice, as well as returning `false`, on failure.
176+
$convertedCharacter = @\iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
177+
if (!\is_string($convertedCharacter)) {
178+
throw new \RuntimeException(
179+
'The Unicode escape sequence could not be converted to the target character set.',
180+
1787112620
181+
);
182+
}
183+
return $convertedCharacter;
168184
}
169185
if ($isForIdentifier) {
170186
$peek = \ord($this->peek());
@@ -204,7 +220,14 @@ public function consumeWhiteSpace(array &$comments = []): string
204220
{
205221
$consumed = '';
206222
do {
207-
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
223+
while (true) {
224+
$matchResult = \preg_match('/\\s/isSu', $this->peek());
225+
if (!\is_int($matchResult)) {
226+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112621);
227+
}
228+
if ($matchResult !== 1) {
229+
break;
230+
}
208231
$consumed .= $this->consume(1);
209232
}
210233
if ($this->parserSettings->usesLenientParsing()) {
@@ -318,7 +341,11 @@ public function consumeExpression(string $expression, ?int $maximumLength = null
318341
{
319342
$matches = null;
320343
$input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
321-
if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
344+
$matchResult = \preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE);
345+
if (!\is_int($matchResult)) {
346+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112622);
347+
}
348+
if ($matchResult !== 1) {
322349
throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
323350
}
324351

@@ -467,7 +494,10 @@ private function strsplit(string $string): array
467494
{
468495
if ($this->parserSettings->hasMultibyteSupport()) {
469496
if ($this->streql($this->charset, 'utf-8')) {
470-
$result = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
497+
$result = \preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
498+
if (!\is_array($result)) {
499+
throw new \RuntimeException('The CSS is not valid UTF-8.', 1787112623);
500+
}
471501
} else {
472502
$length = \mb_strlen($string, $this->charset);
473503
$result = [];

tests/Unit/Parsing/ParserStateTest.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,84 @@
1515
*/
1616
final class ParserStateTest extends TestCase
1717
{
18+
/**
19+
* @test
20+
*/
21+
public function constructorThrowsForTextThatIsNotValidUtf8(): void
22+
{
23+
if (\PHP_VERSION_ID < 70304) {
24+
// https://bugs.php.net/76127
25+
self::markTestSkipped('Before PHP 7.3.4 preg_split did not return false for invalid UTF-8');
26+
}
27+
28+
$this->expectException(\RuntimeException::class);
29+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
30+
$this->expectExceptionCode(1787112623);
31+
32+
new ParserState("\xFF body{}", Settings::create());
33+
}
34+
35+
/**
36+
* @test
37+
*/
38+
public function parseIdentifierThrowsForTextThatIsNotValidUtf8WithoutMultibyteSupport(): void
39+
{
40+
$subject = new ParserState("a\xFF", Settings::create()->withMultibyteSupport(false));
41+
42+
$this->expectException(\RuntimeException::class);
43+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
44+
$this->expectExceptionCode(1787112617);
45+
46+
$subject->parseIdentifier();
47+
}
48+
49+
/**
50+
* @test
51+
*/
52+
public function parseCharacterThrowsForEscapedTextThatIsNotValidUtf8WithoutMultibyteSupport(): void
53+
{
54+
$subject = new ParserState("\\\xFF", Settings::create()->withMultibyteSupport(false));
55+
56+
$this->expectException(\RuntimeException::class);
57+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
58+
$this->expectExceptionCode(1787112618);
59+
60+
$subject->parseCharacter(true);
61+
}
62+
63+
/**
64+
* @test
65+
*/
66+
public function parseCharacterThrowsForMultibyteCharacterAfterIncompleteUnicodeEscapeWithoutMultibyteSupport(): void
67+
{
68+
// "\xC3\xA9" is "é". Without multibyte support, it is split into two separate "characters",
69+
// each of which is not valid UTF-8 on its own.
70+
$subject = new ParserState("\\3\xC3\xA9", Settings::create()->withMultibyteSupport(false));
71+
72+
$this->expectException(\RuntimeException::class);
73+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
74+
$this->expectExceptionCode(1787112619);
75+
76+
$subject->parseCharacter(true);
77+
}
78+
79+
/**
80+
* @test
81+
*/
82+
public function parseCharacterThrowsForEscapedCodePointThatCannotBeConverted(): void
83+
{
84+
// 0xFFFFFF is outside the Unicode range, so `\iconv` cannot convert it.
85+
$subject = new ParserState('\\FFFFFF', Settings::create());
86+
87+
$this->expectException(\RuntimeException::class);
88+
$this->expectExceptionMessage(
89+
'The Unicode escape sequence could not be converted to the target character set.'
90+
);
91+
$this->expectExceptionCode(1787112620);
92+
93+
$subject->parseCharacter(false);
94+
}
95+
1896
/**
1997
* @return array<
2098
* string,
@@ -303,4 +381,32 @@ public function consumeWhiteSpaceStopsAtNonWhitespace(string $nonWhitespace, str
303381

304382
self::assertTrue($subject->comes($nonWhitespace));
305383
}
384+
385+
/**
386+
* @test
387+
*/
388+
public function consumeWhiteSpaceThrowsForTextThatIsNotValidUtf8WithoutMultibyteSupport(): void
389+
{
390+
$subject = new ParserState("\xFF body{}", Settings::create()->withMultibyteSupport(false));
391+
392+
$this->expectException(\RuntimeException::class);
393+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
394+
$this->expectExceptionCode(1787112621);
395+
396+
$subject->consumeWhiteSpace();
397+
}
398+
399+
/**
400+
* @test
401+
*/
402+
public function consumeExpressionThrowsForTextThatIsNotValidUtf8WithoutMultibyteSupport(): void
403+
{
404+
$subject = new ParserState("\xFF", Settings::create()->withMultibyteSupport(false));
405+
406+
$this->expectException(\RuntimeException::class);
407+
$this->expectExceptionMessage('The CSS is not valid UTF-8.');
408+
$this->expectExceptionCode(1787112622);
409+
410+
$subject->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
411+
}
306412
}

0 commit comments

Comments
 (0)