diff --git a/composer.json b/composer.json index aeafe80..dab413c 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require": { "react/event-loop": "~0.4.0", - "react/socket": "~0.4.0" + "react/socket": "~0.4.0", + "nekland/tools": "^1.0" }, "require-dev": { "phpunit/phpunit": "^5.3" diff --git a/src/Exception/Frame/TooBigFrameException.php b/src/Exception/Frame/TooBigFrameException.php index 7477cab..b6cbe4a 100644 --- a/src/Exception/Frame/TooBigFrameException.php +++ b/src/Exception/Frame/TooBigFrameException.php @@ -2,7 +2,7 @@ /** * This file is a part of Woketo package. * - * (c) Ci-tron + * (c) Nekland * * For the full license, take a look to the LICENSE file * on the root directory of this project @@ -10,9 +10,31 @@ namespace Nekland\Woketo\Exception\Frame; +use Exception; use Nekland\Woketo\Exception\LimitationException; class TooBigFrameException extends LimitationException { + /** + * @var int + */ + private $maxLength; + /** + * @param int $maxLength + * @param string $message + */ + public function __construct(int $maxLength, string $message = 'The frame is too big to be processed.') + { + parent::__construct($message, null, null); + $this->maxLength = $maxLength; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } } diff --git a/src/Rfc6455/Frame.php b/src/Rfc6455/Frame.php index 2f89ffb..f2766d8 100644 --- a/src/Rfc6455/Frame.php +++ b/src/Rfc6455/Frame.php @@ -8,6 +8,7 @@ * on the root directory of this project */ +declare(strict_types=1); namespace Nekland\Woketo\Rfc6455; use Nekland\Woketo\Exception\Frame\ControlFrameException; @@ -89,7 +90,7 @@ class Frame * @var int */ private $secondByte; - + /** * @var bool */ @@ -133,11 +134,12 @@ public function __construct($data=null) { if (null !== $data) { $this->setRawData($data); - $this->checkFrameSize(); } } /** + * It also run checks on data. + * * @param string|int $rawData Probably more likely a string than an int, but well... why not. * @return self * @throws InvalidFrameException @@ -145,13 +147,22 @@ public function __construct($data=null) public function setRawData($rawData) { $this->rawData = $rawData; - $this->frameSize = strlen($rawData); + $this->frameSize = BitManipulation::frameSize($rawData); if ($this->frameSize < 2) { throw new InvalidFrameException('Not enough data to be a frame.'); } $this->getInformationFromRawData(); + try { + $this->checkFrameSize(); + } catch (TooBigFrameException $e) { + $this->frameSize = $e->getMaxLength(); + $this->rawData = BitManipulation::bytesFromToString($this->rawData, 0, $this->frameSize, BitManipulation::MODE_PHP); + } + + Frame::checkFrame($this); + return $this; } @@ -188,12 +199,12 @@ public function getRawData() : string } if ($this->isMasked()) { $data .= $this->getMaskingKey(); - $data .= $this->applyMask($this->getPayload()); - - return $data; + $data .= $this->applyMask(); + + return $this->rawData = $data; } - - return $data . $this->getPayload(); + + return $this->rawData = $data . $this->getPayload(); } /** @@ -221,7 +232,7 @@ public function setFinal(bool $final) : Frame */ public function getRsv1() : bool { - return BitManipulation::nthBit($this->firstByte, 2); + return (bool) BitManipulation::nthBit($this->firstByte, 2); } /** @@ -229,7 +240,7 @@ public function getRsv1() : bool */ public function getRsv2() : bool { - return BitManipulation::nthBit($this->firstByte, 3); + return (bool) BitManipulation::nthBit($this->firstByte, 3); } /** @@ -237,7 +248,7 @@ public function getRsv2() : bool */ public function getRsv3() : bool { - return BitManipulation::nthBit($this->firstByte, 4); + return (bool) BitManipulation::nthBit($this->firstByte, 4); } /** @@ -247,15 +258,15 @@ public function getOpcode() : int { return BitManipulation::partOfByte($this->firstByte, 2); } - + public function setOpcode(int $opcode) : Frame { if (!in_array($opcode, [Frame::OP_TEXT, Frame::OP_BINARY, Frame::OP_CLOSE, Frame::OP_CONTINUE, Frame::OP_PING, Frame::OP_PONG])) { throw new \InvalidArgumentException('Wrong opcode !'); } - + $this->opcode = $opcode; - + return $this; } @@ -287,7 +298,7 @@ public function getMaskingKey() : string $value = BitManipulation::bytesFromTo($this->rawData, $start, $start + 3); - return $this->mask = BitManipulation::intToString($value); + return $this->mask = BitManipulation::intToString($value, 4); } public function getPayload() @@ -296,13 +307,13 @@ public function getPayload() return $this->payload; } - $this->checkFrameSize(); - $infoBytesLen = $this->getInfoBytesLen(); - $payload = (string) substr($this->rawData, $infoBytesLen, $this->payloadLen); + $payload = (string) BitManipulation::bytesFromToString($this->rawData, $infoBytesLen, $this->payloadLen, BitManipulation::MODE_PHP); if ($this->isMasked()) { - return $this->payload = $this->applyMask($payload); + $this->payload = $payload; + + return $this->payload = $this->applyMask(); } return $this->payload = $payload; @@ -319,37 +330,18 @@ public function getInfoBytesLen() return $this->infoBytesLen = (9 + $this->payloadLenSize) / 8 + ($this->isMasked() ? 4 : 0); } - public function checkFrameSize() - { - $infoBytesLen = $this->getInfoBytesLen(); - $realDataLength = BitManipulation::frameSize($this->rawData); - $theoricDataLength = $infoBytesLen + $this->payloadLen; - - if ($realDataLength < $theoricDataLength) { - throw new IncompleteFrameException( - sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $realDataLength) - ); - } - - if ($realDataLength > $theoricDataLength) { - throw new TooBigFrameException(); - } - } - - - public function setPayload(string $payload) : Frame { $this->payload = $payload; $this->payloadLen = BitManipulation::frameSize($this->payload); $this->payloadLenSize = 7; - + if ($this->payloadLen > 126 && $this->payloadLen < 65536) { $this->payloadLenSize += 16; } else if ($this->payloadLen > 126) { $this->payloadLenSize += 64; } - + return $this; } @@ -382,7 +374,7 @@ public function getPayloadLength() : int // Check < 0 because 64th bit is the negative one in PHP. if ($payloadLen < 0 || $payloadLen > Frame::$maxPayloadSize) { - throw new TooBigFrameException; + throw new TooBigFrameException(Frame::$maxPayloadSize); } return $this->payloadLen = $payloadLen; @@ -393,22 +385,26 @@ public function isMasked() : bool if ($this->mask !== null) { return true; } - + if ($this->rawData !== null) { return (bool) BitManipulation::nthBit($this->secondByte, 1); } - + return false; } - public function applyMask(string $payload) : string + /** + * This method works for mask and unmask (it's the same operation) + * + * @return string + */ + public function applyMask() : string { $res = ''; $mask = $this->getMaskingKey(); - for ($i = 0; $i < $this->payloadLen; $i++) { - $payloadByte = $payload[$i]; + $payloadByte = $this->payload[$i]; $res .= $payloadByte ^ $mask[$i % 4]; } @@ -422,10 +418,39 @@ private function getInformationFromRawData() $this->final = (bool) BitManipulation::nthBit($this->firstByte, 1); $this->payloadLen = $this->getPayloadLength(); + } - Frame::checkFrame($this); + /** + * Check if the frame have the good size based on payload size. + * + * @throws IncompleteFrameException + * @throws TooBigFrameException + */ + public function checkFrameSize() + { + $infoBytesLen = $this->getInfoBytesLen(); + $this->frameSize = BitManipulation::frameSize($this->rawData); + $theoricDataLength = $infoBytesLen + $this->payloadLen; + + if ($this->frameSize < $theoricDataLength) { + throw new IncompleteFrameException( + sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $this->frameSize) + ); + } + + if ($this->frameSize > $theoricDataLength) { + throw new TooBigFrameException($theoricDataLength); + } } + /** + * Validate a frame with RFC criteria + * + * @param Frame $frame + * @throws ControlFrameException + * @throws InvalidFrameException + * @throws TooBigControlFrameException + */ public static function checkFrame(Frame $frame) { if ($frame->getOpcode() === Frame::OP_TEXT && !mb_check_encoding($frame->getPayload())) { @@ -438,14 +463,14 @@ public static function checkFrame(Frame $frame) } if ($frame->getPayloadLength() > Frame::MAX_CONTROL_FRAME_SIZE) { - throw new TooBigControlFrameException('A control frame cannot be larger than 125 bytes.'); + throw new TooBigControlFrameException(Frame::MAX_CONTROL_FRAME_SIZE, 'A control frame cannot be larger than 125 bytes.'); } } } /** * You can call this method to be sure your frame is valid before trying to get the raw data. - * + * * @return bool */ public function isValid() : bool diff --git a/src/Rfc6455/Message.php b/src/Rfc6455/Message.php index 7296503..3589a01 100644 --- a/src/Rfc6455/Message.php +++ b/src/Rfc6455/Message.php @@ -10,9 +10,11 @@ namespace Nekland\Woketo\Rfc6455; +use Nekland\Tools\StringTools; use Nekland\Woketo\Exception\Frame\IncompleteFrameException; use Nekland\Woketo\Exception\LimitationException; use Nekland\Woketo\Exception\MissingDataException; +use Nekland\Woketo\Utils\BitManipulation; class Message { @@ -40,17 +42,21 @@ public function __construct() public function addData($data) { - try { - if ('' === $this->buffer) { - $this->addFrame(new Frame($data)); - } else { - $this->addFrame(new Frame($this->buffer . $data)); + $this->buffer .= $data; + do { + try { + $this->addFrame($frame = new Frame($this->buffer)); + $this->buffer = StringTools::removeStart($this->buffer, $frame->getRawData(), '8bit'); + } catch (IncompleteFrameException $e) { + return ''; // There is no more frame we can generate, the data is saved as buffer. } - $this->buffer = ''; + } while(!$this->isComplete() && !empty($this->buffer)); - } catch (IncompleteFrameException $e) { - $this->buffer .= $data; + if ($this->isComplete()) { + return $this->buffer; } + + return ''; } /** diff --git a/src/Rfc6455/MessageProcessor.php b/src/Rfc6455/MessageProcessor.php index 95d09cd..ce5ca94 100644 --- a/src/Rfc6455/MessageProcessor.php +++ b/src/Rfc6455/MessageProcessor.php @@ -10,8 +10,10 @@ namespace Nekland\Woketo\Rfc6455; +use Nekland\Tools\StringTools; use Nekland\Woketo\Exception\LimitationException; use Nekland\Woketo\Rfc6455\MessageHandler\Rfc6455MessageHandlerInterface; +use Nekland\Woketo\Utils\BitManipulation; use React\Socket\ConnectionInterface; /** @@ -39,35 +41,41 @@ public function __construct(FrameFactory $factory = null) } /** - * @param string $data + * @param string $data * @param ConnectionInterface $socket - * @param Message|null $message - * @return Message|null + * @param Message|null $message + * @return \Generator */ public function onData(string $data, ConnectionInterface $socket, Message $message = null) { - if (null === $message) { - $message = new Message(); - } + do { - try { - $message->addData($data); - if ($message->isComplete()) { - foreach ($this->handlers as $handler) { - if ($handler->supports($message)) { - $handler->process($message, $this, $socket); - return null; - } - } + if (null === $message) { + $message = new Message(); } - return $message; - } catch (LimitationException $e) { - $this->write($this->frameFactory->createCloseFrame(Frame::CLOSE_TOO_BIG_TO_PROCESS), $socket); - $socket->end(); - } + try { + $data = $message->addData($data); - return null; + if ($message->isComplete()) { + foreach ($this->handlers as $handler) { + if ($handler->supports($message)) { + $handler->process($message, $this, $socket); + } + } + + yield $message; + $message = null; + } else { + yield $message; + } + + } catch (LimitationException $e) { + $this->write($this->frameFactory->createCloseFrame(Frame::CLOSE_TOO_BIG_TO_PROCESS), $socket); + $socket->end(); + $data = ''; + } + } while(!empty($data)); } /** diff --git a/src/Server/Connection.php b/src/Server/Connection.php index e9ed51c..3d3f802 100644 --- a/src/Server/Connection.php +++ b/src/Server/Connection.php @@ -118,26 +118,28 @@ protected function processMessage($data) $this->timeout = null; } - $this->currentMessage = $this->messageProcessor->onData($data, $this->socketStream, $this->currentMessage); - - if (null !== $this->currentMessage && $this->currentMessage->isComplete()) { - // Sending the message through the woketo API. - switch($this->currentMessage->getOpcode()) { - case Frame::OP_TEXT: - $this->handler->onMessage($this->currentMessage->getContent(), $this); - break; - case Frame::OP_BINARY: - $this->handler->onBinary($this->currentMessage->getContent(), $this); - break; - } - $this->currentMessage = null; + foreach ($this->messageProcessor->onData($data, $this->socketStream, $this->currentMessage) as $message) { + $this->currentMessage = $message; + if ($this->currentMessage->isComplete()) { + // Sending the message through the woketo API. + switch($this->currentMessage->getOpcode()) { + case Frame::OP_TEXT: + $this->handler->onMessage($this->currentMessage->getContent(), $this); + break; + case Frame::OP_BINARY: + $this->handler->onBinary($this->currentMessage->getContent(), $this); + break; + } + $this->currentMessage = null; - } else if (null !== $this->currentMessage && !$this->currentMessage->isComplete()) { - // We wait for more data so we start a timeout. - $this->timeout = $this->loop->addTimer(Connection::DEFAULT_TIMEOUT, function () { - $this->messageProcessor->timeout($this->socketStream); - }); + } else { + // We wait for more data so we start a timeout. + $this->timeout = $this->loop->addTimer(Connection::DEFAULT_TIMEOUT, function () { + $this->messageProcessor->timeout($this->socketStream); + }); + } } + } /** diff --git a/src/Utils/BitManipulation.php b/src/Utils/BitManipulation.php index 69c44d0..ed64f18 100644 --- a/src/Utils/BitManipulation.php +++ b/src/Utils/BitManipulation.php @@ -20,6 +20,12 @@ */ class BitManipulation { + /** + * Mode from to is the default mode of inspection of frames. But PHP usually uses from and length to inspect frames. + */ + const MODE_FROM_TO = 0; + const MODE_PHP = 1; + /** * Get a specific bit from a byte. * @@ -193,12 +199,29 @@ public static function bytesFromTo($frame, int $from, int $to, bool $force8bytes return $res; } + /** + * Proxy to the substr to be sure to be use the right method (mb_substr) + * + * @param string $frame + * @param int $from + * @param int $to + * @return string + */ + public static function bytesFromToString(string $frame, int $from, int $to, int $mode = BitManipulation::MODE_FROM_TO) : string + { + if ($mode === BitManipulation::MODE_FROM_TO) { + return mb_substr($frame, $from, $to - $from + 1, '8bit'); + } + + return mb_substr($frame, $from, $to, '8bit'); + } + /** * Take a frame represented by a decimal int to transform it in a string. * Notice that any int is a frame and cannot be more than 8 bytes * * @param int $frame - * @param int|null $size In bytes. + * @param int|null $size In bytes. This value should always be precise. Be careful if you don't ! * @return string */ public static function intToString(int $frame, int $size = null) : string diff --git a/tests/Woketo/Core/ConnectionTest.php b/tests/Woketo/Core/ConnectionTest.php index 42ae3b2..9de203d 100644 --- a/tests/Woketo/Core/ConnectionTest.php +++ b/tests/Woketo/Core/ConnectionTest.php @@ -44,7 +44,7 @@ public function testItSupportsTextMessage() $message->isComplete()->willReturn(true); $message->getOpcode()->willReturn(Frame::OP_TEXT); $message->getContent()->willReturn('Hello'); - $processor->onData($helloFrame, Argument::cetera())->willReturn($message->reveal()); + $processor->onData($helloFrame, Argument::cetera())->willReturn([$message->reveal()]); $handler->onConnection(Argument::type(Connection::class))->shouldBeCalled(); $handler->onMessage('Hello', Argument::type(Connection::class))->shouldBeCalled(); @@ -76,7 +76,7 @@ public function testItSupportsBinaryMessage() $message->isComplete()->willReturn(true); $message->getOpcode()->willReturn(Frame::OP_BINARY); $message->getContent()->willReturn($binary); - $processor->onData($binaryFrame, Argument::cetera())->willReturn($message->reveal()); + $processor->onData($binaryFrame, Argument::cetera())->willReturn([$message->reveal()]); $handler->onConnection(Argument::type(Connection::class))->shouldBeCalled(); $handler->onBinary($binary, Argument::type(Connection::class))->shouldBeCalled(); diff --git a/tests/Woketo/Rfc6455/FrameTest.php b/tests/Woketo/Rfc6455/FrameTest.php index 4bc6b2d..0e02586 100644 --- a/tests/Woketo/Rfc6455/FrameTest.php +++ b/tests/Woketo/Rfc6455/FrameTest.php @@ -79,14 +79,14 @@ public function testPingUnmaskedFrameContainingHello() public function testPongMaskedFrameContainingHello() { - $helloUnmaskedPingFrame = new Frame( - BitManipulation::hexArrayToString('8a', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58') - ); + $raw = BitManipulation::hexArrayToString('8a', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + $helloUnmaskedPingFrame = new Frame($raw); $this->assertSame($helloUnmaskedPingFrame->isMasked(), true); $this->assertSame($helloUnmaskedPingFrame->isFinal(), true); $this->assertSame($helloUnmaskedPingFrame->getPayload(), 'Hello'); $this->assertSame($helloUnmaskedPingFrame->getOpcode(), Frame::OP_PONG); + $this->assertSame($helloUnmaskedPingFrame->getRawData(), $raw); } public function testItSupportsEmptyFrames() @@ -100,6 +100,18 @@ public function testItSupportsEmptyFrames() ])); } + public function testItTakesOnlyFirstWebsocketFrameFromEntryData() + { + // 2 frames masked with `Hello` as content + $entryData = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + $firstDataFrame = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + + $frame = new Frame($entryData); + + $this->assertSame($frame->getRawData(), $firstDataFrame); + $this->assertSame($frame->getPayload(), 'Hello'); + } + /** * @dataProvider frameDataGenerationTestProvider * @@ -134,6 +146,16 @@ public function testItGenerateFrameWith65536Bytes() $this->assertSame($expectedData, $frame->getRawData()); } + public function testItSupportsFrameWith65536PayloadFromRawData() + { + $payload = file_get_contents(__DIR__ . '/../../fixtures/65536.data'); + $rawData = BitManipulation::hexArrayToString('81', '7F', '00', '00', '00', '00', '00', '01', '00', '00') . $payload; + + $frame = new Frame($rawData); + + $this->assertSame($rawData, $frame->getRawData()); + } + public function frameDataGenerationTestProvider() { return [ diff --git a/tests/Woketo/Rfc6455/MessageProcessorTest.php b/tests/Woketo/Rfc6455/MessageProcessorTest.php index 4b3d0cf..0eb0ab8 100644 --- a/tests/Woketo/Rfc6455/MessageProcessorTest.php +++ b/tests/Woketo/Rfc6455/MessageProcessorTest.php @@ -15,6 +15,7 @@ use Nekland\Woketo\Rfc6455\Frame; use Nekland\Woketo\Rfc6455\FrameFactory; use Nekland\Woketo\Rfc6455\Message; +use Nekland\Woketo\Rfc6455\MessageHandler\PingFrameHandler; use Nekland\Woketo\Rfc6455\MessageHandler\Rfc6455MessageHandlerInterface; use Nekland\Woketo\Rfc6455\MessageProcessor; use Nekland\Woketo\Utils\BitManipulation; @@ -37,14 +38,47 @@ public function testItBuildMessagesUsingMessageClass() $processor = new MessageProcessor(); /** @var Message $message */ - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( // Hello normal frame BitManipulation::hexArrayToString(['81', '05', '48', '65', '6c', '6c', '6f']), $this->socket->reveal() + )); + + $this->assertInstanceOf(Message::class, $messages[0]); + $this->assertSame('Hello', $messages[0]->getContent()); + } + + public function testItBuildManyMessagesWithOnlyOneFrameData() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '01', '03', '48', '65', '6c', // Data part 1 + '80', '02', '6c', '6f', // Data part 2 + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58' // Another message (Hello frame) + ); + + $processor = new MessageProcessor(); + + $messages = iterator_to_array($processor->onData($multipleFrameData, $this->socket->reveal())); + + $this->assertSame(count($messages), 2); + $this->assertSame($messages[1]->getContent(), 'Hello'); + } + + public function testItContinueFrameEvaluationAfterControlFrame() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '89', '00', // Ping + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', // Another message (Hello frame) + '89', '00' // Ping ); - $this->assertInstanceOf(Message::class, $message); - $this->assertSame('Hello', $message->getContent()); + $processor = new MessageProcessor(); + $processor->addHandler(new PingFrameHandler()); + + $messages = iterator_to_array($processor->onData($multipleFrameData, $this->socket->reveal())); + + $this->assertSame(count($messages), 3); + $this->assertSame($messages[1]->getContent(), 'Hello'); } public function testItBuildPartialMessage() @@ -52,23 +86,23 @@ public function testItBuildPartialMessage() $processor = new MessageProcessor(); $socket = $this->socket->reveal(); - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( // "Hel" normal frame unmasked BitManipulation::hexArrayToString(['01', '03', '48', '65', '6c']), $socket - ); + )); - $this->assertSame($message->isComplete(), false); + $this->assertSame($messages[0]->isComplete(), false); - $processor->onData( + iterator_to_array($processor->onData( // "lo" normal frame unmasked BitManipulation::hexArrayToString(['80', '02', '6c', '6f']), $socket, - $message - ); + $messages[0] + )); - $this->assertSame($message->isComplete(), true); - $this->assertSame($message->getContent(), 'Hello'); + $this->assertSame($messages[0]->isComplete(), true); + $this->assertSame($messages[0]->getContent(), 'Hello'); } public function testItHandleSpecialMessagesWithHandler() @@ -88,12 +122,12 @@ public function process(Message $message, MessageProcessor $messageProcessor, Co $this->socket->write(Argument::cetera())->shouldBeCalled(); - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( BitManipulation::hexArrayToString(['88', '02', '03', 'E8']), $this->socket->reveal() - ); + )); - $this->assertSame($message, null); + $this->assertSame($messages[0]->getOpcode(), Frame::OP_CLOSE); } public function testItReturnTheFrameFactory() diff --git a/tests/Woketo/Rfc6455/MessageTest.php b/tests/Woketo/Rfc6455/MessageTest.php index 3051399..853d15c 100644 --- a/tests/Woketo/Rfc6455/MessageTest.php +++ b/tests/Woketo/Rfc6455/MessageTest.php @@ -40,6 +40,40 @@ public function testItStackFramesAndReturnCompleteMessage() $this->assertSame($message->getContent(), 'foo bar baz'); } + public function testItReturnUnusedDataOnAddDataCall() + { + // 2 frames "Hello" + $entryData = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + + $message = new Message(); + $data = $message->addData($entryData); + + $this->assertSame($data, BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58')); + } + + public function testItCompleteMessageWithMultipleFramesWhenDataAllowIt() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '01', '03', '48', '65', '6c', // Data part 1 + '80', '02', '6c', '6f', // Data part 2 + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58' // Another message (Hello frame) + ); + + $message = new Message(); + + $this->assertSame($message->addData($multipleFrameData), BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58')); + } + + public function testItReturnNothingWhenBufferingWhenAddData() + { + $incompleteFrame = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d'); + + $message = new Message(); + + $this->assertSame($message->addData($incompleteFrame), ''); + $this->assertSame($message->isComplete(), false); + } + public function testItThrowErrorWhenMissingFrame() { /** @var Frame $frame1 */ diff --git a/tests/Woketo/Utils/BitManipulationTest.php b/tests/Woketo/Utils/BitManipulationTest.php index f5ca671..3dc59de 100644 --- a/tests/Woketo/Utils/BitManipulationTest.php +++ b/tests/Woketo/Utils/BitManipulationTest.php @@ -119,6 +119,12 @@ public function testItTransformToHex($a, $b) $this->assertSame($a, BitManipulation::frameToHex($b)); } + public function testItRetrieveSubFrames() + { + $this->assertSame('bcd', BitManipulation::bytesFromToString('abcdefg', 1, 3)); + $this->assertSame('ef', BitManipulation::bytesFromToString('abcdefg', 4, 2, BitManipulation::MODE_PHP)); + } + // // Providers //