mirror of
https://github.com/predis/predis.git
synced 2026-09-16 13:27:14 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3edc442e55 | |||
| 37490865cc | |||
| 3a5b55e46e | |||
| 401abc4315 | |||
| e7b89c14b7 | |||
| 3a8c350f3d | |||
| 675951360f | |||
| 0795d69d9e |
@@ -13,7 +13,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
- name: Check Spelling
|
- name: Check Spelling
|
||||||
uses: rojopolis/spellcheck-github-actions@0.63.0
|
uses: rojopolis/spellcheck-github-actions@0.66.0
|
||||||
with:
|
with:
|
||||||
config_path: .github/spellcheck-settings.yml
|
config_path: .github/spellcheck-settings.yml
|
||||||
task_name: Markdown
|
task_name: Markdown
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ jobs:
|
|||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Start Redis standalone image
|
- name: Start Redis standalone image
|
||||||
uses: hoverkraft-tech/compose-action@v3.0.0
|
uses: hoverkraft-tech/compose-action@v3.1.0
|
||||||
with:
|
with:
|
||||||
compose-file: .github/docker-compose.yml
|
compose-file: .github/docker-compose.yml
|
||||||
services: ${{ env.DOCKER_SERVICE }}
|
services: ${{ env.DOCKER_SERVICE }}
|
||||||
|
|
||||||
- name: Start Redis unprotected image
|
- name: Start Redis unprotected image
|
||||||
uses: hoverkraft-tech/compose-action@v3.0.0
|
uses: hoverkraft-tech/compose-action@v3.1.0
|
||||||
if: ${{ matrix.redis > '4.0' }}
|
if: ${{ matrix.redis > '4.0' }}
|
||||||
with:
|
with:
|
||||||
compose-file: .github/docker-compose.yml
|
compose-file: .github/docker-compose.yml
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start Redis stack image
|
- name: Start Redis stack image
|
||||||
id: stack_infra
|
id: stack_infra
|
||||||
uses: hoverkraft-tech/compose-action@v3.0.0
|
uses: hoverkraft-tech/compose-action@v3.1.0
|
||||||
if: ${{ matrix.redis >= '7.2' && matrix.redis < '8.0' }}
|
if: ${{ matrix.redis >= '7.2' && matrix.redis < '8.0' }}
|
||||||
with:
|
with:
|
||||||
compose-file: .github/docker-compose.yml
|
compose-file: .github/docker-compose.yml
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start Redis cluster image
|
- name: Start Redis cluster image
|
||||||
id: cluster_infra
|
id: cluster_infra
|
||||||
uses: hoverkraft-tech/compose-action@v3.0.0
|
uses: hoverkraft-tech/compose-action@v3.1.0
|
||||||
if: ${{ matrix.redis > '4.0' }}
|
if: ${{ matrix.redis > '4.0' }}
|
||||||
with:
|
with:
|
||||||
compose-file: .github/docker-compose.yml
|
compose-file: .github/docker-compose.yml
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start Redis sentinels image
|
- name: Start Redis sentinels image
|
||||||
id: sentinel_infra
|
id: sentinel_infra
|
||||||
uses: hoverkraft-tech/compose-action@v3.0.0
|
uses: hoverkraft-tech/compose-action@v3.1.0
|
||||||
if: ${{ matrix.redis > '4.0' }}
|
if: ${{ matrix.redis > '4.0' }}
|
||||||
with:
|
with:
|
||||||
compose-file: .github/docker-compose.yml
|
compose-file: .github/docker-compose.yml
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### Added
|
||||||
|
### Changed
|
||||||
|
- Deprecated `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786)
|
||||||
|
### Fixed
|
||||||
|
- Fixed RESP3 double parsing returning positive `INF` for `-inf` payloads (#1716)
|
||||||
|
- Fixed `client_info` connection parameter being ignored (#1722)
|
||||||
|
- Fixed `Stream::write()`/`read()` leaving a dead connection when a host error handler throws exception (#1725)
|
||||||
|
- Fixed CRLF command smuggling and node misrouting in `AbstractAggregateConnection::write()` and `CommandInterface::deserializeCommand()` (CVE GHSA-w6f5-v2h6-g786)
|
||||||
|
|
||||||
## v3.6.0 (2026-08-14)
|
## v3.6.0 (2026-08-14)
|
||||||
### Added
|
### Added
|
||||||
- Added support for new TS commands + Indonesian language support integration test (#1695)
|
- Added support for new TS commands + Indonesian language support integration test (#1695)
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ use Traversable;
|
|||||||
*/
|
*/
|
||||||
class Client implements ClientInterface, IteratorAggregate
|
class Client implements ClientInterface, IteratorAggregate
|
||||||
{
|
{
|
||||||
public const VERSION = '3.6.0';
|
public const VERSION = '3.6.1-dev';
|
||||||
|
|
||||||
/** @var OptionsInterface */
|
/** @var OptionsInterface */
|
||||||
private $options;
|
private $options;
|
||||||
|
|||||||
+51
-14
@@ -158,15 +158,14 @@ abstract class Command implements CommandInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @deprecated Not binary-safe; see CommandInterface::deserializeCommand().
|
||||||
|
* Scheduled for removal in the next major.
|
||||||
*/
|
*/
|
||||||
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
||||||
{
|
{
|
||||||
if ($serializedCommand[0] !== '*') {
|
$items = self::parseMultibulk($serializedCommand);
|
||||||
throw new UnexpectedValueException('Invalid serializing format');
|
$commandId = $items[0];
|
||||||
}
|
|
||||||
|
|
||||||
$commandArray = explode("\r\n", $serializedCommand);
|
|
||||||
$commandId = $commandArray[2];
|
|
||||||
$classPath = __NAMESPACE__ . '\Redis\\';
|
$classPath = __NAMESPACE__ . '\Redis\\';
|
||||||
|
|
||||||
// Check if given command is a module command.
|
// Check if given command is a module command.
|
||||||
@@ -186,15 +185,53 @@ abstract class Command implements CommandInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$command = new $classPath();
|
$command = new $classPath();
|
||||||
$arguments = [];
|
$command->setArguments(array_slice($items, 1));
|
||||||
|
|
||||||
for ($i = 4, $iMax = count($commandArray); $i < $iMax; $i++) {
|
|
||||||
$arguments[] = $commandArray[$i];
|
|
||||||
++$i;
|
|
||||||
}
|
|
||||||
|
|
||||||
$command->setArguments($arguments);
|
|
||||||
|
|
||||||
return $command;
|
return $command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a RESP multibulk buffer into its individual bulk-string values
|
||||||
|
* (command ID followed by its arguments), walking each string by its own
|
||||||
|
* declared byte length instead of splitting the buffer on "\r\n" -- which
|
||||||
|
* a bulk string's payload may legitimately contain (see GHSA-w6f5-v2h6-g786).
|
||||||
|
*
|
||||||
|
* @param string $buffer
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private static function parseMultibulk(string $buffer): array
|
||||||
|
{
|
||||||
|
if ($buffer[0] !== '*') {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineEnd = strpos($buffer, "\r\n");
|
||||||
|
|
||||||
|
if ($lineEnd === false) {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = (int) substr($buffer, 1, $lineEnd - 1);
|
||||||
|
$offset = $lineEnd + 2;
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; ++$i) {
|
||||||
|
if (($buffer[$offset] ?? '') !== '$') {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||||
|
|
||||||
|
if ($lineEnd === false) {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||||
|
$dataStart = $lineEnd + 2;
|
||||||
|
$items[] = substr($buffer, $dataStart, $bulkLen);
|
||||||
|
$offset = $dataStart + $bulkLen + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ interface CommandInterface
|
|||||||
*
|
*
|
||||||
* @param string $serializedCommand
|
* @param string $serializedCommand
|
||||||
* @return static
|
* @return static
|
||||||
|
*
|
||||||
|
* @deprecated Not binary-safe: it re-parses on "\r\n" and ignores RESP bulk-length
|
||||||
|
* prefixes, so any argument containing "\r\n" is corrupted, and it
|
||||||
|
* instantiates a command class from the parsed input. Never call it on
|
||||||
|
* untrusted or serialized data (see CVE GHSA-w6f5-v2h6-g786). Scheduled
|
||||||
|
* for removal in the next major.
|
||||||
*/
|
*/
|
||||||
public static function deserializeCommand(string $serializedCommand): CommandInterface;
|
public static function deserializeCommand(string $serializedCommand): CommandInterface;
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-14
@@ -153,14 +153,16 @@ final class RawCommand implements CommandInterface
|
|||||||
return $buffer;
|
return $buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @deprecated Not binary-safe; see CommandInterface::deserializeCommand().
|
||||||
|
* Scheduled for removal in the next major.
|
||||||
|
*/
|
||||||
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
public static function deserializeCommand(string $serializedCommand): CommandInterface
|
||||||
{
|
{
|
||||||
if ($serializedCommand[0] !== '*') {
|
$items = self::parseMultibulk($serializedCommand);
|
||||||
throw new UnexpectedValueException('Invalid serializing format');
|
$commandId = $items[0];
|
||||||
}
|
|
||||||
|
|
||||||
$commandArray = explode("\r\n", $serializedCommand);
|
|
||||||
$commandId = $commandArray[2];
|
|
||||||
$classPath = __NAMESPACE__ . '\Redis\\';
|
$classPath = __NAMESPACE__ . '\Redis\\';
|
||||||
|
|
||||||
// Check if given command is a module command.
|
// Check if given command is a module command.
|
||||||
@@ -180,15 +182,53 @@ final class RawCommand implements CommandInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$command = new $classPath();
|
$command = new $classPath();
|
||||||
$arguments = [];
|
$command->setArguments(array_slice($items, 1));
|
||||||
|
|
||||||
for ($i = 4, $iMax = count($commandArray); $i < $iMax; $i++) {
|
|
||||||
$arguments[] = $commandArray[$i];
|
|
||||||
++$i;
|
|
||||||
}
|
|
||||||
|
|
||||||
$command->setArguments($arguments);
|
|
||||||
|
|
||||||
return $command;
|
return $command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a RESP multibulk buffer into its individual bulk-string values
|
||||||
|
* (command ID followed by its arguments), walking each string by its own
|
||||||
|
* declared byte length instead of splitting the buffer on "\r\n" -- which
|
||||||
|
* a bulk string's payload may legitimately contain (see GHSA-w6f5-v2h6-g786).
|
||||||
|
*
|
||||||
|
* @param string $buffer
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
private static function parseMultibulk(string $buffer): array
|
||||||
|
{
|
||||||
|
if ($buffer[0] !== '*') {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineEnd = strpos($buffer, "\r\n");
|
||||||
|
|
||||||
|
if ($lineEnd === false) {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = (int) substr($buffer, 1, $lineEnd - 1);
|
||||||
|
$offset = $lineEnd + 2;
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; ++$i) {
|
||||||
|
if (($buffer[$offset] ?? '') !== '$') {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||||
|
|
||||||
|
if ($lineEnd === false) {
|
||||||
|
throw new UnexpectedValueException('Invalid serializing format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||||
|
$dataStart = $lineEnd + 2;
|
||||||
|
$items[] = substr($buffer, $dataStart, $bulkLen);
|
||||||
|
$offset = $dataStart + $bulkLen + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,17 +77,25 @@ abstract class AbstractAggregateConnection implements AggregateConnectionInterfa
|
|||||||
*/
|
*/
|
||||||
public function write(string $buffer): void
|
public function write(string $buffer): void
|
||||||
{
|
{
|
||||||
$rawCommands = [];
|
$offset = 0;
|
||||||
$explodedBuffer = explode("\r\n", trim($buffer));
|
$length = strlen($buffer);
|
||||||
|
|
||||||
while (!empty($explodedBuffer)) {
|
while ($offset < $length) {
|
||||||
$argsLen = (int) explode('*', $explodedBuffer[0])[1];
|
$start = $offset;
|
||||||
$cmdLen = ($argsLen * 2) + 1;
|
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||||
$rawCommands[] = array_splice($explodedBuffer, 0, $cmdLen);
|
$argsCount = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||||
}
|
$offset = $lineEnd + 2;
|
||||||
|
|
||||||
foreach ($rawCommands as $command) {
|
// Advance by each bulk string's own declared byte length rather than
|
||||||
$command = implode("\r\n", $command) . "\r\n";
|
// splitting on literal "\r\n", which a bulk string's value may legitimately
|
||||||
|
// contain (see GHSA-w6f5-v2h6-g786).
|
||||||
|
for ($i = 0; $i < $argsCount; ++$i) {
|
||||||
|
$lineEnd = strpos($buffer, "\r\n", $offset);
|
||||||
|
$bulkLen = (int) substr($buffer, $offset + 1, $lineEnd - $offset - 1);
|
||||||
|
$offset = $lineEnd + 2 + $bulkLen + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = substr($buffer, $start, $offset - $start);
|
||||||
$commandObj = Command::deserializeCommand($command);
|
$commandObj = Command::deserializeCommand($command);
|
||||||
$this->getConnectionByCommand($commandObj)->write($command);
|
$this->getConnectionByCommand($commandObj)->write($command);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,13 +208,15 @@ class Factory implements FactoryInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$connection->addConnectCommand(
|
if ($parameters->client_info ?? true) {
|
||||||
new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', $this->buildLibraryName()])
|
$connection->addConnectCommand(
|
||||||
);
|
new RawCommand('CLIENT', ['SETINFO', 'LIB-NAME', $this->buildLibraryName()])
|
||||||
|
);
|
||||||
|
|
||||||
$connection->addConnectCommand(
|
$connection->addConnectCommand(
|
||||||
new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION])
|
new RawCommand('CLIENT', ['SETINFO', 'LIB-VER', Client::VERSION])
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($parameters->database) && strlen($parameters->database)) {
|
if (isset($parameters->database) && strlen($parameters->database)) {
|
||||||
$connection->addConnectCommand(
|
$connection->addConnectCommand(
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ use Predis\Retry\Retry;
|
|||||||
* @property string $database Database index (see the SELECT command).
|
* @property string $database Database index (see the SELECT command).
|
||||||
* @property bool $async_connect Performs the connect() operation asynchronously.
|
* @property bool $async_connect Performs the connect() operation asynchronously.
|
||||||
* @property bool $tcp_nodelay Toggles the Nagle's algorithm for coalescing.
|
* @property bool $tcp_nodelay Toggles the Nagle's algorithm for coalescing.
|
||||||
* @property bool $client_info Whether to set LIB-NAME and LIB-VER when connecting.
|
* @property bool $client_info Whether to set LIB-NAME and LIB-VER when connecting, enabled by default.
|
||||||
* @property Retry $retry Retry configuration
|
* @property Retry $retry Retry configuration
|
||||||
* @property bool $cache (Relay only) Whether to use in-memory caching.
|
* @property bool $cache (Relay only) Whether to use in-memory caching.
|
||||||
* @property string $serializer (Relay only) Serializer used for data serialization.
|
* @property string $serializer (Relay only) Serializer used for data serialization.
|
||||||
|
|||||||
@@ -205,7 +205,10 @@ class Stream implements StreamInterface
|
|||||||
throw new RuntimeException('Cannot write to a non-writable stream');
|
throw new RuntimeException('Cannot write to a non-writable stream');
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = fwrite($this->stream, $string);
|
// Suppressed: some error handlers (Laravel, Symfony, Laminas) convert engine
|
||||||
|
// notices/warnings into thrown exceptions, which would otherwise bypass the
|
||||||
|
// return-value handling below and leave a dead connection looking "connected".
|
||||||
|
$result = @fwrite($this->stream, $string);
|
||||||
|
|
||||||
if ($result === false || $result === 0) {
|
if ($result === false || $result === 0) {
|
||||||
$metadata = $this->getMetadata();
|
$metadata = $this->getMetadata();
|
||||||
@@ -265,10 +268,11 @@ class Stream implements StreamInterface
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suppressed: see the note in write() above.
|
||||||
if ($length === -1) {
|
if ($length === -1) {
|
||||||
$string = fgets($this->stream);
|
$string = @fgets($this->stream);
|
||||||
} else {
|
} else {
|
||||||
$string = fread($this->stream, $length);
|
$string = @fread($this->stream, $length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $string) {
|
if (false === $string) {
|
||||||
|
|||||||
@@ -63,10 +63,14 @@ class Resp3Strategy extends Resp2Strategy
|
|||||||
*/
|
*/
|
||||||
protected function parseDouble(string $string): float
|
protected function parseDouble(string $string): float
|
||||||
{
|
{
|
||||||
if ($string === 'inf' || $string === '-inf') {
|
if ($string === 'inf') {
|
||||||
return INF;
|
return INF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($string === '-inf') {
|
||||||
|
return -INF;
|
||||||
|
}
|
||||||
|
|
||||||
return (float) $string;
|
return (float) $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,17 @@ class CommandTest extends PredisTestCase
|
|||||||
TOPKQUERY::class,
|
TOPKQUERY::class,
|
||||||
['key'],
|
['key'],
|
||||||
],
|
],
|
||||||
|
// Regression cases for GHSA-w6f5-v2h6-g786 (CWE-93): a "\r\n" embedded
|
||||||
|
// in a bulk string's own byte payload must not be mistaken for a RESP
|
||||||
|
// line boundary.
|
||||||
|
'GET with CRLF embedded in the value' => [
|
||||||
|
GET::class,
|
||||||
|
["value\r\n*1\r\n\$4\r\nEVIL"],
|
||||||
|
],
|
||||||
|
'ZADD with CRLF embedded in the key' => [
|
||||||
|
ZADD::class,
|
||||||
|
["key\r\n*1\r\n\$4\r\nEVIL", 'value', 'key1', 'value1'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,17 @@ class RawCommandTest extends PredisTestCase
|
|||||||
TOPKQUERY::class,
|
TOPKQUERY::class,
|
||||||
['key'],
|
['key'],
|
||||||
],
|
],
|
||||||
|
// Regression cases for GHSA-w6f5-v2h6-g786 (CWE-93): a "\r\n" embedded
|
||||||
|
// in a bulk string's own byte payload must not be mistaken for a RESP
|
||||||
|
// line boundary.
|
||||||
|
'GET with CRLF embedded in the value' => [
|
||||||
|
GET::class,
|
||||||
|
["value\r\n*1\r\n\$4\r\nEVIL"],
|
||||||
|
],
|
||||||
|
'ZADD with CRLF embedded in the key' => [
|
||||||
|
ZADD::class,
|
||||||
|
["key\r\n*1\r\n\$4\r\nEVIL", 'value', 'key1', 'value1'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace Predis\Connection\Cluster;
|
|||||||
|
|
||||||
use Predis\Command\CommandInterface;
|
use Predis\Command\CommandInterface;
|
||||||
use Predis\Command\Redis\GET;
|
use Predis\Command\Redis\GET;
|
||||||
|
use Predis\Command\Redis\SET;
|
||||||
use Predis\Connection\Parameters;
|
use Predis\Connection\Parameters;
|
||||||
use PredisTestCase;
|
use PredisTestCase;
|
||||||
|
|
||||||
@@ -516,4 +517,73 @@ class PredisClusterTest extends PredisTestCase
|
|||||||
|
|
||||||
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string's own value must not be mistaken for a command boundary
|
||||||
|
* (splitting one command into a smuggled extra command), and must not
|
||||||
|
* corrupt the argument list used to pick the target node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||||
|
{
|
||||||
|
$command = new SET();
|
||||||
|
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||||
|
|
||||||
|
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||||
|
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||||
|
|
||||||
|
$cluster = new PredisCluster(new Parameters());
|
||||||
|
$cluster->add($connection1);
|
||||||
|
$cluster->add($connection2);
|
||||||
|
|
||||||
|
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||||
|
$otherConnection = $expectedConnection === $connection1 ? $connection2 : $connection1;
|
||||||
|
|
||||||
|
$expectedConnection
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$otherConnection
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$cluster->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string KEY must not corrupt the argument list used to pick the
|
||||||
|
* target node, which would silently route the command to the wrong node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||||
|
{
|
||||||
|
$command = new SET();
|
||||||
|
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||||
|
|
||||||
|
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||||
|
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||||
|
|
||||||
|
$cluster = new PredisCluster(new Parameters());
|
||||||
|
$cluster->add($connection1);
|
||||||
|
$cluster->add($connection2);
|
||||||
|
|
||||||
|
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||||
|
$otherConnection = $expectedConnection === $connection1 ? $connection2 : $connection1;
|
||||||
|
|
||||||
|
$expectedConnection
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$otherConnection
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$cluster->write($command->serializeCommand());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1676,4 +1676,77 @@ class RedisClusterTest extends PredisTestCase
|
|||||||
|
|
||||||
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
$cluster->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string's own value must not be mistaken for a command boundary
|
||||||
|
* (splitting one command into a smuggled extra command), and must not
|
||||||
|
* corrupt the argument list used to pick the target node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||||
|
|
||||||
|
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
|
||||||
|
|
||||||
|
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||||
|
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||||
|
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||||
|
|
||||||
|
$cluster = new RedisCluster($factory, new Parameters());
|
||||||
|
$cluster->add($connection1);
|
||||||
|
$cluster->add($connection2);
|
||||||
|
$cluster->add($connection3);
|
||||||
|
|
||||||
|
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||||
|
|
||||||
|
foreach ([$connection1, $connection2, $connection3] as $connection) {
|
||||||
|
if ($connection === $expectedConnection) {
|
||||||
|
$connection->expects($this->once())->method('write')->with($command->serializeCommand());
|
||||||
|
} else {
|
||||||
|
$connection->expects($this->never())->method('write');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cluster->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string KEY must not corrupt the argument list used to pick the
|
||||||
|
* target node, which would silently route the command to the wrong node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||||
|
|
||||||
|
$factory = $this->getMockBuilder(FactoryInterface::class)->getMock();
|
||||||
|
|
||||||
|
$connection1 = $this->getMockConnection('tcp://127.0.0.1:7001');
|
||||||
|
$connection2 = $this->getMockConnection('tcp://127.0.0.1:7002');
|
||||||
|
$connection3 = $this->getMockConnection('tcp://127.0.0.1:7003');
|
||||||
|
|
||||||
|
$cluster = new RedisCluster($factory, new Parameters());
|
||||||
|
$cluster->add($connection1);
|
||||||
|
$cluster->add($connection2);
|
||||||
|
$cluster->add($connection3);
|
||||||
|
|
||||||
|
$expectedConnection = $cluster->getConnectionByCommand($command);
|
||||||
|
|
||||||
|
foreach ([$connection1, $connection2, $connection3] as $connection) {
|
||||||
|
if ($connection === $expectedConnection) {
|
||||||
|
$connection->expects($this->once())->method('write')->with($command->serializeCommand());
|
||||||
|
} else {
|
||||||
|
$connection->expects($this->never())->method('write');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cluster->write($command->serializeCommand());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,6 +585,24 @@ class FactoryTest extends PredisTestCase
|
|||||||
$this->assertSame(['SETINFO', 'LIB-VER', Client::VERSION], $initCommands[2]->getArguments());
|
$this->assertSame(['SETINFO', 'LIB-VER', Client::VERSION], $initCommands[2]->getArguments());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group disconnected
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testDoesNotSetClientNameAndVersionOnConnectionWithClientInfoDisabled(): void
|
||||||
|
{
|
||||||
|
$parameters = ['client_info' => false];
|
||||||
|
|
||||||
|
$factory = new Factory();
|
||||||
|
$connection = $factory->create($parameters);
|
||||||
|
$initCommands = $connection->getInitCommands();
|
||||||
|
|
||||||
|
$this->assertCount(1, $initCommands);
|
||||||
|
$this->assertInstanceOf(RawCommand::class, $initCommands[0]);
|
||||||
|
$this->assertSame('HELLO', $initCommands[0]->getId());
|
||||||
|
$this->assertSame([2, 'SETNAME', 'predis'], $initCommands[0]->getArguments());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group disconnected
|
* @group disconnected
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1496,6 +1496,69 @@ repl_backlog_histlen:12978
|
|||||||
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string's own value must not be mistaken for a command boundary
|
||||||
|
* (splitting one command into a smuggled extra command).
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||||
|
|
||||||
|
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||||
|
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||||
|
|
||||||
|
$slave1
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$replication = new MasterSlaveReplication();
|
||||||
|
$replication->add($master);
|
||||||
|
$replication->add($slave1);
|
||||||
|
|
||||||
|
$replication->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string KEY must not corrupt the argument list used to pick the
|
||||||
|
* target connection, which would silently route the command to the
|
||||||
|
* wrong node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||||
|
|
||||||
|
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||||
|
$slave1 = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||||
|
|
||||||
|
$slave1
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$replication = new MasterSlaveReplication();
|
||||||
|
$replication->add($master);
|
||||||
|
$replication->add($slave1);
|
||||||
|
|
||||||
|
$replication->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @medium
|
* @medium
|
||||||
* @group disconnected
|
* @group disconnected
|
||||||
|
|||||||
@@ -2020,6 +2020,85 @@ class SentinelReplicationTest extends PredisTestCase
|
|||||||
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
$replication->write($command1->serializeCommand() . $command2->serializeCommand() . $command3->serializeCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string's own value must not be mistaken for a command boundary
|
||||||
|
* (splitting one command into a smuggled extra command).
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringValue(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(['victim-key', "PAD\r\n*1\r\n\$7\r\nFLUSHDB"]);
|
||||||
|
|
||||||
|
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
|
||||||
|
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||||
|
$slave = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||||
|
$strategy = new Replication\ReplicationStrategy();
|
||||||
|
$factory = new Connection\Factory();
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('isConnected')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$slave
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
|
||||||
|
$replication->add($master);
|
||||||
|
$replication->add($slave);
|
||||||
|
|
||||||
|
$replication->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for GHSA-w6f5-v2h6-g786 (CWE-93): a CRLF embedded in a
|
||||||
|
* bulk string KEY must not corrupt the argument list used to pick the
|
||||||
|
* target connection, which would silently route the command to the
|
||||||
|
* wrong node.
|
||||||
|
*
|
||||||
|
* @group disconnected
|
||||||
|
*/
|
||||||
|
public function testWriteHandlesCRLFEmbeddedInBulkStringKey(): void
|
||||||
|
{
|
||||||
|
$command = new Command\Redis\SET();
|
||||||
|
$command->setArguments(["victim\r\n*1\r\n\$4\r\nEVIL", 'somevalue']);
|
||||||
|
|
||||||
|
$sentinel = $this->getMockSentinelConnection('tcp://127.0.0.1:5381?role=sentinel');
|
||||||
|
$master = $this->getMockConnection('tcp://127.0.0.1:6379?role=master');
|
||||||
|
$slave = $this->getMockConnection('tcp://127.0.0.1:6380?role=slave');
|
||||||
|
$strategy = new Replication\ReplicationStrategy();
|
||||||
|
$factory = new Connection\Factory();
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('isConnected')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$slave
|
||||||
|
->expects($this->never())
|
||||||
|
->method('write');
|
||||||
|
|
||||||
|
$master
|
||||||
|
->expects($this->once())
|
||||||
|
->method('write')
|
||||||
|
->with($command->serializeCommand());
|
||||||
|
|
||||||
|
$replication = new SentinelReplication('svc', [$sentinel], $factory, $strategy);
|
||||||
|
$replication->add($master);
|
||||||
|
$replication->add($slave);
|
||||||
|
|
||||||
|
$replication->write($command->serializeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
public function connectionsProvider(): array
|
public function connectionsProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
namespace Predis\Connection\Resource;
|
namespace Predis\Connection\Resource;
|
||||||
|
|
||||||
|
use ErrorException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
@@ -431,6 +432,99 @@ class StreamTest extends TestCase
|
|||||||
$stream->write('');
|
$stream->write('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testWriteSuppressesEngineWarningUnderThrowingErrorHandler(): void
|
||||||
|
{
|
||||||
|
$this->registerEngineWarningWrapper();
|
||||||
|
$this->installThrowingErrorHandler();
|
||||||
|
|
||||||
|
$stream = new Stream(fopen('predis-test-engine-warning://x', 'r+'));
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Unable to write to stream');
|
||||||
|
|
||||||
|
$stream->write('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function testReadSuppressesEngineWarningUnderThrowingErrorHandler(): void
|
||||||
|
{
|
||||||
|
$this->registerEngineWarningWrapper();
|
||||||
|
$this->installThrowingErrorHandler();
|
||||||
|
|
||||||
|
$stream = new Stream(fopen('predis-test-engine-warning://x', 'r+'));
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Unable to read from stream');
|
||||||
|
|
||||||
|
// Use the fgets() path (length = -1): PHP 7.2's fread() coerces a
|
||||||
|
// user stream wrapper's `false` return into an empty string instead
|
||||||
|
// of preserving it, which would make this assertion PHP-version
|
||||||
|
// dependent; fgets() doesn't have that quirk.
|
||||||
|
$stream->read(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mimics error handlers installed by Laravel, Symfony and Laminas, which
|
||||||
|
* convert engine notices/warnings into thrown exceptions unless the call
|
||||||
|
* site suppressed them with `@` (see GH-1725). The runner's own ambient
|
||||||
|
* error_reporting() level is irrelevant to what we want to assert here,
|
||||||
|
* so it's pinned to a known, fully-enabled value for the duration of the
|
||||||
|
* test rather than trusted as-is.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function installThrowingErrorHandler(): void
|
||||||
|
{
|
||||||
|
$this->originalErrorReporting = error_reporting(E_ALL);
|
||||||
|
|
||||||
|
set_error_handler(static function ($level, $message, $file = '', $line = 0) {
|
||||||
|
if (error_reporting() & $level) {
|
||||||
|
throw new ErrorException($message, 0, $level, $file, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->registeredErrorHandler = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $registeredErrorHandler = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int|null
|
||||||
|
*/
|
||||||
|
private $originalErrorReporting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if ($this->registeredErrorHandler) {
|
||||||
|
restore_error_handler();
|
||||||
|
error_reporting($this->originalErrorReporting);
|
||||||
|
$this->registeredErrorHandler = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function registerEngineWarningWrapper(): void
|
||||||
|
{
|
||||||
|
if (!in_array('predis-test-engine-warning', stream_get_wrappers(), true)) {
|
||||||
|
stream_wrapper_register('predis-test-engine-warning', EngineWarningStreamWrapperFixture::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function writableModeProvider(): array
|
public function writableModeProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -478,3 +572,56 @@ class StreamTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper fixture that raises an engine-style warning from
|
||||||
|
* stream_write()/stream_read(), used to verify that Stream::write()/read()
|
||||||
|
* suppress it instead of letting a host-installed error handler turn it
|
||||||
|
* into an uncaught exception (see GH-1725).
|
||||||
|
*/
|
||||||
|
class EngineWarningStreamWrapperFixture
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
public $context;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$openedPath): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int|bool
|
||||||
|
*/
|
||||||
|
public function stream_write(string $data)
|
||||||
|
{
|
||||||
|
trigger_error('fwrite(): synthetic broken pipe', E_USER_WARNING);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string|bool
|
||||||
|
*/
|
||||||
|
public function stream_read(int $count)
|
||||||
|
{
|
||||||
|
trigger_error('fread(): synthetic broken pipe', E_USER_WARNING);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ class Resp3StrategyTest extends PredisTestCase
|
|||||||
* @param string $data
|
* @param string $data
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data): void
|
public function testParseDataReturnsFloatInfinityOnInfinityOrNegativeInfinity(string $data, float $expectedValue): void
|
||||||
{
|
{
|
||||||
$actualResponse = $this->strategy->parseData($data);
|
$actualResponse = $this->strategy->parseData($data);
|
||||||
|
|
||||||
$this->assertInfinite($actualResponse);
|
$this->assertSame($expectedValue, $actualResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,8 +166,8 @@ class Resp3StrategyTest extends PredisTestCase
|
|||||||
public function infinityProvider(): array
|
public function infinityProvider(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'positive infinity' => [",inf\r\n"],
|
'positive infinity' => [",inf\r\n", INF],
|
||||||
'negative infinity' => [",-inf\r\n"],
|
'negative infinity' => [",-inf\r\n", -INF],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user