diff --git a/docs/component/str.md b/docs/component/str.md index 45b24198..e35583bb 100644 --- a/docs/component/str.md +++ b/docs/component/str.md @@ -53,7 +53,7 @@ - [range](./../../src/Psl/Str/range.php#L41) - [repeat](./../../src/Psl/Str/repeat.php#L26) - [replace](./../../src/Psl/Str/replace.php#L15) -- [replace_ci](./../../src/Psl/Str/replace_ci.php#L16) +- [replace_ci](./../../src/Psl/Str/replace_ci.php#L20) - [replace_every](./../../src/Psl/Str/replace_every.php#L15) - [replace_every_ci](./../../src/Psl/Str/replace_every_ci.php#L15) - [reverse](./../../src/Psl/Str/reverse.php#L14) @@ -69,9 +69,9 @@ - [strip_prefix](./../../src/Psl/Str/strip_prefix.php#L13) - [strip_suffix](./../../src/Psl/Str/strip_suffix.php#L13) - [to_int](./../../src/Psl/Str/to_int.php#L12) -- [trim](./../../src/Psl/Str/trim.php#L18) -- [trim_left](./../../src/Psl/Str/trim_left.php#L18) -- [trim_right](./../../src/Psl/Str/trim_right.php#L18) +- [trim](./../../src/Psl/Str/trim.php#L21) +- [trim_left](./../../src/Psl/Str/trim_left.php#L21) +- [trim_right](./../../src/Psl/Str/trim_right.php#L21) - [truncate](./../../src/Psl/Str/truncate.php#L25) - [uppercase](./../../src/Psl/Str/uppercase.php#L14) - [width](./../../src/Psl/Str/width.php#L14) diff --git a/src/Psl/Encoding/Base64/Internal/Base64.php b/src/Psl/Encoding/Base64/Internal/Base64.php index a115dd6c..10ec2005 100644 --- a/src/Psl/Encoding/Base64/Internal/Base64.php +++ b/src/Psl/Encoding/Base64/Internal/Base64.php @@ -102,6 +102,8 @@ public static function decode(string $base64, bool $explicit_padding = true): st if ($explicit_padding && $base64_length % 4 !== 0) { throw new Exception\IncorrectPaddingException('The given base64 string has incorrect padding.'); } + + /** @psalm-suppress MissingThrowsDocblock */ $base64 = Str\trim_right($base64, '='); $base64_length = Str\length($base64, encoding: Str\Encoding::ASCII_8BIT); diff --git a/src/Psl/Str/replace_ci.php b/src/Psl/Str/replace_ci.php index d548ba26..4b2c5137 100644 --- a/src/Psl/Str/replace_ci.php +++ b/src/Psl/Str/replace_ci.php @@ -4,6 +4,8 @@ namespace Psl\Str; +use Psl\Regex; + use function preg_quote; use function preg_split; @@ -12,6 +14,8 @@ * `$replacement` (case-insensitive). * * @pure + * + * @throws Exception\InvalidArgumentException if $needle is not a valid UTF-8 string. */ function replace_ci(string $haystack, string $needle, string $replacement, Encoding $encoding = Encoding::UTF_8): string { @@ -19,5 +23,15 @@ function replace_ci(string $haystack, string $needle, string $replacement, Encod return $haystack; } - return join(preg_split('{' . preg_quote($needle, '/') . '}iu', $haystack), $replacement); + try { + /** @var list */ + $pieces = Regex\Internal\call_preg( + 'preg_split', + static fn() => preg_split('{' . preg_quote($needle, '/') . '}iu', $haystack, -1), + ); + } catch (Regex\Exception\RuntimeException | Regex\Exception\InvalidPatternException $error) { + throw new Exception\InvalidArgumentException($error->getMessage(), previous: $error); + } + + return join($pieces, $replacement); } diff --git a/src/Psl/Str/trim.php b/src/Psl/Str/trim.php index 4cec0a34..f3b41b3e 100644 --- a/src/Psl/Str/trim.php +++ b/src/Psl/Str/trim.php @@ -4,8 +4,9 @@ namespace Psl\Str; +use Psl\Regex; + use function preg_quote; -use function preg_replace; /** * Returns the given string with whitespace stripped from the beginning and end. @@ -14,11 +15,17 @@ * be stripped: space, tab, newline, carriage return, NUL byte, vertical tab. * * @pure + * + * @throws Exception\InvalidArgumentException if $string is not a valid UTF-8 string. */ function trim(string $string, ?string $char_mask = null): string { $char_mask ??= " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; $char_mask = preg_quote($char_mask, null); - return preg_replace("{^[{$char_mask}]++|[{$char_mask}]++$}uD", '', $string); + try { + return Regex\replace($string, "{^[{$char_mask}]++|[{$char_mask}]++$}uD", ''); + } catch (Regex\Exception\RuntimeException | Regex\Exception\InvalidPatternException $error) { + throw new Exception\InvalidArgumentException($error->getMessage(), previous: $error); + } } diff --git a/src/Psl/Str/trim_left.php b/src/Psl/Str/trim_left.php index 50110d11..c7f62a92 100644 --- a/src/Psl/Str/trim_left.php +++ b/src/Psl/Str/trim_left.php @@ -4,8 +4,9 @@ namespace Psl\Str; +use Psl\Regex; + use function preg_quote; -use function preg_replace; /** * Returns the given string with whitespace stripped from the left. @@ -14,11 +15,17 @@ * be stripped: space, tab, newline, carriage return, NUL byte, vertical tab. * * @pure + * + * @throws Exception\InvalidArgumentException if $string is not a valid UTF-8 string. */ function trim_left(string $string, ?string $char_mask = null): string { $char_mask ??= " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; $char_mask = preg_quote($char_mask, null); - return preg_replace("{^[{$char_mask}]++}uD", '', $string); + try { + return Regex\replace($string, "{^[{$char_mask}]++}uD", ''); + } catch (Regex\Exception\RuntimeException | Regex\Exception\InvalidPatternException $error) { + throw new Exception\InvalidArgumentException($error->getMessage(), previous: $error); + } } diff --git a/src/Psl/Str/trim_right.php b/src/Psl/Str/trim_right.php index cb744207..8bdd1e36 100644 --- a/src/Psl/Str/trim_right.php +++ b/src/Psl/Str/trim_right.php @@ -4,8 +4,9 @@ namespace Psl\Str; +use Psl\Regex; + use function preg_quote; -use function preg_replace; /** * Returns the given string with whitespace stripped from the right. @@ -14,11 +15,17 @@ * be stripped: space, tab, newline, carriage return, NUL byte, vertical tab. * * @pure + * + * @throws Exception\InvalidArgumentException if $string is not a valid UTF-8 string. */ function trim_right(string $string, ?string $char_mask = null): string { $char_mask ??= " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"; $char_mask = preg_quote($char_mask, null); - return preg_replace("{[{$char_mask}]++$}uD", '', $string); + try { + return Regex\replace($string, "{[{$char_mask}]++$}uD", ''); + } catch (Regex\Exception\RuntimeException | Regex\Exception\InvalidPatternException $error) { + throw new Exception\InvalidArgumentException($error->getMessage(), previous: $error); + } } diff --git a/src/Psl/Type/Internal/LiteralScalarType.php b/src/Psl/Type/Internal/LiteralScalarType.php index c1f2e94f..3560c57e 100644 --- a/src/Psl/Type/Internal/LiteralScalarType.php +++ b/src/Psl/Type/Internal/LiteralScalarType.php @@ -122,6 +122,7 @@ public function toString(): string } if (Type\float()->matches($value)) { + /** @psalm-suppress MissingThrowsDocblock */ $string_representation = Str\trim_right(Str\format('%.14F', $value), '0'); /** @psalm-suppress MissingThrowsDocblock */ if (Str\ends_with($string_representation, '.')) { diff --git a/src/Psl/Type/Internal/PositiveIntType.php b/src/Psl/Type/Internal/PositiveIntType.php index 3a56ffe8..b9a54ae9 100644 --- a/src/Psl/Type/Internal/PositiveIntType.php +++ b/src/Psl/Type/Internal/PositiveIntType.php @@ -47,8 +47,13 @@ public function coerce(mixed $value): int return $int; } - $trimmed = Str\trim_left($str, '0'); - $int = Str\to_int($trimmed); + try { + $trimmed = Str\trim_left($str, '0'); + } catch (Str\Exception\InvalidArgumentException $e) { + throw CoercionException::withValue($value, $this->toString(), $this->getTrace()); + } + + $int = Str\to_int($trimmed); if (null !== $int && $int > 0) { return $int; } diff --git a/tests/unit/Str/ReplaceCiTest.php b/tests/unit/Str/ReplaceCiTest.php index 8e8e3d9c..852f67c3 100644 --- a/tests/unit/Str/ReplaceCiTest.php +++ b/tests/unit/Str/ReplaceCiTest.php @@ -26,4 +26,36 @@ public function provideData(): array ['foo', 'foo', 'bar', 'baz'], ]; } + + /** + * @dataProvider provideBadUtf8Data + */ + public function testBadUtf8(string $string, string $expectedException, string $expectedExceptionMessage): void + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + Str\replace_ci($string, $string, $string); + } + + public function provideBadUtf8Data(): iterable + { + yield [ + "\xc1\xbf", + Str\Exception\InvalidArgumentException::class, + 'Compilation failed: UTF-8 error: overlong 2-byte sequence at offset 0', + ]; + + yield [ + "\xe0\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Compilation failed: UTF-8 error: overlong 3-byte sequence at offset 0', + ]; + + yield [ + "\xf0\x80\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Compilation failed: UTF-8 error: overlong 4-byte sequence at offset 0', + ]; + } } diff --git a/tests/unit/Str/TrimLeftTest.php b/tests/unit/Str/TrimLeftTest.php index 9a6256c9..45790842 100644 --- a/tests/unit/Str/TrimLeftTest.php +++ b/tests/unit/Str/TrimLeftTest.php @@ -47,4 +47,36 @@ public function provideData(): array ], ]; } + + /** + * @dataProvider provideBadUtf8Data + */ + public function testBadUtf8(string $string, string $expectedException, string $expectedExceptionMessage): void + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + Str\trim_left($string); + } + + public function provideBadUtf8Data(): iterable + { + yield [ + "\xc1\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xe0\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xf0\x80\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + } } diff --git a/tests/unit/Str/TrimRightTest.php b/tests/unit/Str/TrimRightTest.php index d1ffffcd..c2269283 100644 --- a/tests/unit/Str/TrimRightTest.php +++ b/tests/unit/Str/TrimRightTest.php @@ -57,4 +57,36 @@ public function provideData(): array ], ]; } + + /** + * @dataProvider provideBadUtf8Data + */ + public function testBadUtf8(string $string, string $expectedException, string $expectedExceptionMessage): void + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + Str\trim_right($string); + } + + public function provideBadUtf8Data(): iterable + { + yield [ + "\xc1\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xe0\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xf0\x80\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + } } diff --git a/tests/unit/Str/TrimTest.php b/tests/unit/Str/TrimTest.php index e0e80d99..03369a46 100644 --- a/tests/unit/Str/TrimTest.php +++ b/tests/unit/Str/TrimTest.php @@ -52,4 +52,36 @@ public function provideData(): array ], ]; } + + /** + * @dataProvider provideBadUtf8Data + */ + public function testBadUtf8(string $string, string $expectedException, string $expectedExceptionMessage): void + { + $this->expectException($expectedException); + $this->expectExceptionMessage($expectedExceptionMessage); + + Str\trim($string); + } + + public function provideBadUtf8Data(): iterable + { + yield [ + "\xc1\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xe0\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + yield [ + "\xf0\x80\x81\xbf", + Str\Exception\InvalidArgumentException::class, + 'Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + } } diff --git a/tests/unit/Type/PositiveIntTypeTest.php b/tests/unit/Type/PositiveIntTypeTest.php index 42a360af..6fc300e7 100644 --- a/tests/unit/Type/PositiveIntTypeTest.php +++ b/tests/unit/Type/PositiveIntTypeTest.php @@ -59,6 +59,7 @@ public function getInvalidCoercions(): iterable yield [$this->stringable('-9223372036854775809')]; yield ['0xFF']; yield ['-0xFF']; + yield ["\xc1\xbf"]; yield ['']; }