diff --git a/src/APIConnector.php b/src/APIConnector.php new file mode 100644 index 0000000..cee50ed --- /dev/null +++ b/src/APIConnector.php @@ -0,0 +1,56 @@ +stream = $stream; + } + + /** + * Reads a WORD from the stream + * + * WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content. + * Length of the WORD should be given as count of bytes that are going to be sent + * + * @return string The word content, en empty string for end of SENTENCE + */ + public function readWord() : string + { + // Get length of next word + $length = APILengthCoDec::decodeLength($this->stream); + if ($length>0) { + return $this->stream->read($length); + } + return ''; + } + + public function writeWord(string $word) + { + $encodedLength = APILengthCoDec::encodeLength(strlen($word)); + return $this->stream->write($encodedLength.$word); + } +} diff --git a/src/APILengthCoDec.php b/src/APILengthCoDec.php new file mode 100644 index 0000000..b3a59d2 --- /dev/null +++ b/src/APILengthCoDec.php @@ -0,0 +1,197 @@ + 7 bits set to 1) + // - encode length with one byte + // - set the byte to length value, as length maximal value is 7 bits set to 1, the most significant bit is always 0 + // - end + // - length <= 0x3FFF (binary : 00111111 11111111 => 14 bits set to 1) + // - encode length with two bytes + // - set length value to 0x8000 (=> 10000000 00000000) + // - add length : as length maximumal value is 14 bits to 1, this does not modify the 2 most significants bits (10) + // - end + // => minimal encoded value is 10000000 10000000 + // - length <= 0x1FFFFF (binary : 00011111 11111111 11111111 => 21 bits set to 1) + // - encode length with three bytes + // - set length value to 0xC00000 (binary : 11000000 00000000 00000000) + // - add length : as length maximal vlaue is 21 bits to 1, this does not modify the 3 most significants bits (110) + // - end + // => minimal encoded value is 11000000 01000000 00000000 + // - length <= 0x0FFFFFFF (binary : 00001111 11111111 11111111 11111111 => 28 bits set to 1) + // - encode length with four bytes + // - set length value to 0xE0000000 (binary : 11100000 00000000 00000000 00000000) + // - add length : as length maximal vlaue is 28 bits to 1, this does not modify the 4 most significants bits (1110) + // - end + // => minimal encoded value is 11100000 00100000 00000000 00000000 + // - length <= 0x7FFFFFFFFF (binary : 00000111 11111111 11111111 11111111 11111111 => 35 bits set to 1) + // - encode length with five bytes + // - set length value to 0xF000000000 (binary : 11110000 00000000 00000000 00000000 00000000) + // - add length : as length maximal vlaue is 35 bits to 1, this does not modify the 5 most significants bits (11110) + // - end + // - length > 0x7FFFFFFFFF : not supported + + + if ($length<0) + { + throw new \DomainException(sprintf("Length of word can not be negative (%d)", $length)); + } + + if ($length<=0x7F) { + return BinaryStringHelper::IntegerToNBOBinaryString($length); + } + else if ($length<=0x3FFF) { + return BinaryStringHelper::IntegerToNBOBinaryString(0x8000+$length); + } + else if ($length<=0x1FFFFF) { + return BinaryStringHelper::IntegerToNBOBinaryString(0xC00000+$length); + } + else if ($length<=0x0FFFFFFF){ + return BinaryStringHelper::IntegerToNBOBinaryString(0xE0000000+$length); + } + // cannot compare with 0x7FFFFFFFFF on 32 bits systems + else { + if (PHP_INT_SIZE<8) { + // Cannot be done on 32 bits systems + // PHP5 windows versions of php, even on 64 bits systems was impacted + // see : https://stackoverflow.com/questions/27865340/php-int-size-returns-4-but-my-operating-system-is-64-bit + + // @codeCoverageIgnoreStart + throw new \OverflowException(sprintf("Your system is using 32 bits integers, cannot encode length of %d bytes on this system", $length)); + // @codeCoverageIgnoreEnd + } + if ($length<=0x7FFFFFFFFF) + { + return BinaryStringHelper::IntegerToNBOBinaryString(0xF000000000+$length); + } + } + throw new \DomainException(sprintf('Length of word too huge (%x)', $length)); + } + + // Decode length of data when reading : + // The 5 firsts bits of the first byte specify how the length is encoded. + // The position of the first 0 value bit, starting from the most significant postion. + // - 0xxxxxxx => The 7 remainings bits of the first byte is the length : + // => min value of length is 0x00 + // => max value of length is 0x7F (127 bytes) + // - 10xxxxxx => The 6 remainings bits of the first byte plus the next byte represent the lenght + // NOTE : the next byte MUST be at least 0x80 !! + // => min value of length is 0x80 + // => max value of length is 0x3FFF (16,383 bytes, near 16 KB) + // - 110xxxxx => The 5 remainings bits of th first byte and the two next bytes represent the length + // => max value of length is 0x1FFFFF (2,097,151 bytes, near 2 MB) + // - 1110xxxx => The 4 remainings bits of the first byte and the three next bytes represent the length + // => max value of length is 0xFFFFFFF (268,435,455 bytes, near 270 MB) + // - 11110xxx => The 3 remainings bits of the first byte and the four next bytes represent the length + // => max value of length is 0x7FFFFFFF (2,147,483,647 byes, 2GB) + // - 11111xxx => This byte is not a length-encoded word but a control byte. + // => Extracted from Mikrotik API doc : + // it is a reserved control byte. + // After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes + // Currently control bytes are not used + + public static function decodeLength(StreamInterface $stream) + { + // if (false === is_resource($stream)) { + // throw new \InvalidArgumentException( + // sprintf( + // 'Argument must be a stream resource type. %s given.', + // gettype($stream) + // ) + // ); + // } + + // Read first byte + $firstByte = ord($stream->read(1)); + + // If first byte is not set, length is the value of the byte + if (0==($firstByte&0x80)) { + return $firstByte; + } + + // if 10xxxxxx, length is 2 bytes encoded + if (0x80==($firstByte&0xC0)) + { + // Set 2 most significants bits to 0 + $result = $firstByte & 0x3F; + // shift left 8 bits to have 2 bytes + $result = $result << 8; + // read next byte and use it as least significant + $result |= ord($stream->read(1)); + return $result; + } + + // if 110xxxxx, length is 3 bytes encoded + if (0xC0 == ($firstByte & 0xE0)) + { + // Set 3 most significants bits to 0 + $result = $firstByte & 0x1F; + // shift left 16 bits to have 3 bytes + $result = $result << 16; + // read next 2 bytes as value and use it as least significant position + $result |= (ord($stream->read(1)) << 8); + $result |= ord($stream->read(1)); + return $result; + } + + // if 1110xxxx, length is 4 bytes encoded + if (0xE0 == ($firstByte & 0xF0)) + { + // Set 4 most significants bits to 0 + $result = $firstByte & 0x0F; + // shift left 24 bits to have 4 bytes + $result = $result << 24; + // read next 3 bytes as value and use it as least significant position + $result |= (ord($stream->read(1)) << 16); + $result |= (ord($stream->read(1)) << 8); + $result |= ord($stream->read(1)); + return $result; + } + + // if 11110xxx, length is 5 bytes encoded + if (0xF0 == ($firstByte & 0xF8)) + { + // Not possibe on 32 bits systems + if (PHP_INT_SIZE<8) { + // Cannot be done on 32 bits systems + // PHP5 windows versions of php, even on 64 bits systems was impacted + // see : https://stackoverflow.com/questions/27865340/php-int-size-returns-4-but-my-operating-system-is-64-bit + // How can we test it ? + // @codeCoverageIgnoreStart + throw new \OverflowException(sprintf("Your system is using 32 bits integers, cannot decode this value (%x) on this system", $firstByte)); + // @codeCoverageIgnoreEnd + } + // Set 5 most significants bits to 0 + $result = $firstByte & 0x07; + // shift left 232 bits to have 5 bytes + $result = $result << 32; + // read next 4 bytes as value and use it as least significant position + $result |= (ord($stream->read(1)) << 24); + $result |= (ord($stream->read(1)) << 16); + $result |= (ord($stream->read(1)) << 8); + $result |= ord($stream->read(1)); + return $result; + } + + // Now the only solution is 5 most significants bits are set to 1 (11111xxx) + // This is a control word, not implemented by Mikrotik for the moment + throw new \UnexpectedValueException("Control Word found\n"); + } +} \ No newline at end of file diff --git a/src/Client.php b/src/Client.php index bf3925d..e9f7553 100644 --- a/src/Client.php +++ b/src/Client.php @@ -46,6 +46,14 @@ class Client implements Interfaces\ClientInterface private $_config; /** + * API communication object + * + * @var APIConnector + */ + + private $connector; + + /** * Client constructor. * * @param array|\RouterOS\Config $config @@ -109,86 +117,6 @@ class Client implements Interfaces\ClientInterface } /** - * Encode given length in RouterOS format - * - * @param string $string - * @return string Encoded length - * @throws \RouterOS\Exceptions\ClientException - */ - private function encodeLength(string $string): string - { - $length = \strlen($string); - - if ($length < 128) { - $orig_length = $length; - $offset = -1; - } elseif ($length < 16384) { - $orig_length = $length | 0x8000; - $offset = -2; - } elseif ($length < 2097152) { - $orig_length = $length | 0xC00000; - $offset = -3; - } elseif ($length < 268435456) { - $orig_length = $length | 0xE0000000; - $offset = -4; - } else { - throw new ClientException("Unable to encode length of '$string'"); - } - - // Pack string to binary format - $result = pack('I*', $orig_length); - // Parse binary string to array - $result = str_split($result); - // Reverse array - $result = array_reverse($result); - // Extract values from offset to end of array - $result = \array_slice($result, $offset); - - // Sew items into one line - $output = null; - foreach ($result as $item) { - $output .= $item; - } - - return $output; - } - - /** - * Read length of line - * - * @param int $byte - * @return int - */ - private function getLength(int $byte): int - { - // If the first bit is set then we need to remove the first four bits, shift left 8 - // and then read another byte in. - // We repeat this for the second and third bits. - // If the fourth bit is set, we need to remove anything left in the first byte - // and then read in yet another byte. - if ($byte & 128) { - if (($byte & 192) === 128) { - $length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1)); - } elseif (($byte & 224) === 192) { - $length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1)); - $length = ($length << 8) + \ord(fread($this->_socket, 1)); - } elseif (($byte & 240) === 224) { - $length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1)); - $length = ($length << 8) + \ord(fread($this->_socket, 1)); - $length = ($length << 8) + \ord(fread($this->_socket, 1)); - } else { - $length = \ord(fread($this->_socket, 1)); - $length = ($length << 8) + \ord(fread($this->_socket, 1)) * 3; - $length = ($length << 8) + \ord(fread($this->_socket, 1)); - $length = ($length << 8) + \ord(fread($this->_socket, 1)); - } - } else { - $length = $byte; - } - return $length; - } - - /** * Send write query to RouterOS (with or without tag) * * @param string|array|\RouterOS\Query $query @@ -211,12 +139,11 @@ class Client implements Interfaces\ClientInterface // Send commands via loop to router foreach ($query->getQuery() as $command) { - $command = trim($command); - fwrite($this->_socket, $this->encodeLength($command) . $command); + $this->connector->writeWord(trim($command)); } - // Write zero-terminator - fwrite($this->_socket, \chr(0)); + // Write zero-terminator (empty string) + $this->connector->writeWord(''); return $this; } @@ -242,12 +169,9 @@ class Client implements Interfaces\ClientInterface // Read answer from socket in loop while (true) { - // Read the first byte of input which gives us some or all of the length - // of the remaining reply. - $byte = fread($this->_socket, 1); - $length = $this->getLength(\ord($byte)); + $word = $this->connector->readWord(); - if ($length == 0) { + if (''===$word) { if ($lastReply) { // We received a !done or !fatal message in a precedent loop // response is complete @@ -260,12 +184,12 @@ class Client implements Interfaces\ClientInterface } // Save output line to response array - $response[] = $line = stream_get_contents($this->_socket, $length); + $response[] = $word; // If we get a !done or !fatal line in response, we are now ready to finish the read // but we need to wait a 0 length message, switch the flag - if ('!done' === $line || '!fatal' === $line) { - $lastReply = true; + if ('!done' === $word || '!fatal' === $word) { + $lastReply = true; } } @@ -449,7 +373,7 @@ class Client implements Interfaces\ClientInterface // If socket is active if (null !== $this->getSocket()) { - + $this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket())); // If we logged in then exit from loop if (true === $this->login()) { $connected = true; @@ -458,6 +382,7 @@ class Client implements Interfaces\ClientInterface // Else close socket and start from begin $this->closeSocket(); + $this->stream = null; } // Sleep some time between tries diff --git a/src/Exceptions/StreamException.php b/src/Exceptions/StreamException.php new file mode 100644 index 0000000..c7dca3e --- /dev/null +++ b/src/Exceptions/StreamException.php @@ -0,0 +1,14 @@ + chr(0x0F).chr(0xF7) + * 0x12345678 => chr(0x12).chr(0x34).chr(0x56).chr(0x76) + * Compatible with 8, 16, 32, 64 etc.. bits systems + * + * @see https://en.wikipedia.org/wiki/Endianness + * @param int $value the integer value to be converted + * @return string the binary string + */ + public static function IntegerToNBOBinaryString(int $value) + { + // Initialize an empty string + $buffer = ''; + // Lets start from the most significant byte + for ($i=(PHP_INT_SIZE-1); $i>=0; $i--) { + // Prepare a mask to keep only the most significant byte of $value + $mask = 0xFF << ($i*8); + // If the most significant byte is not 0, the final string must contain it + // If we have already started to construct the string (i.e. there are more signficant digits) + // we must set the byte, even if it is a 0. + // 0xFF00FF, for example, require to set the second byte byte with a 0 value + if (($value & $mask) || strlen($buffer)!=0) { + // Get the curent byte by shifting it to least significant position and add it to the string + // 0xFF12345678 => 0xFF + $byte = $value>>(8*$i); + $buffer .= chr($byte); + // Set the most significant byte to 0 so we can restart the process being shure + // that the value is left padded with 0 + // 0xFF12345678 => 0x12345678 + // -1 = 0xFFFFF.... (number of F depend of PHP_INT_SIZE ) + $mask = -1 >> ((PHP_INT_SIZE-$i)*8); + $value &= $mask; + } + } + // Special case, 0 will not fill the buffer, have to construct it manualy + if (0==$value) { + $buffer = chr(0); + } + return $buffer; + } +} \ No newline at end of file diff --git a/src/Interfaces/StreamInterface.php b/src/Interfaces/StreamInterface.php new file mode 100644 index 0000000..2832a37 --- /dev/null +++ b/src/Interfaces/StreamInterface.php @@ -0,0 +1,39 @@ +stream = $stream; + } + + /** + * {@inheritDoc} + * @throws \InvalidArgumentException + */ + public function read(int $length) : string + { + if ($length<=0) { + throw new \InvalidArgumentException("Cannot read zero ot negative count of bytes from a stream"); + } + + $result = @fread($this->stream, $length); + + if (false === $result) { + throw new StreamException(sprintf("Error reading %d bytes", $length)); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + public function write(string $string, $length=null) : int + { + if (is_null($length)) { + $length = strlen($string); + } + $result = @fwrite($this->stream, $string, $length); + if (false === $result) { + throw new StreamException(sprintf("Error writing %d bytes", $length)); + } + return $result; + } + + public function close() + { + $hasBeenClosed = false; + if (!is_null($this->stream)) { + $hasBeenClosed = @fclose($this->stream); + $this->stream=null; + } + if (false===$hasBeenClosed) { + throw new StreamException("Error closing stream"); + + } + } +} \ No newline at end of file diff --git a/src/Streams/StringStream.php b/src/Streams/StringStream.php new file mode 100644 index 0000000..11fbe06 --- /dev/null +++ b/src/Streams/StringStream.php @@ -0,0 +1,93 @@ +buffer = $string; + } + + /** + * {@inheritDoc} + * + * @throws \InvalidArgumentException when length parameter is invalid + * @throws StreamException when the stream have been tatly red and read methd is called again + */ + public function read(int $length) : string + { + $remaining = strlen($this->buffer); + + if ($length<0) { + throw new \InvalidArgumentException("Cannot read a negative count of bytes from a stream"); + } + + if (0 == $remaining) { + throw new StreamException("End of stream"); + } + + if ($length>=$remaining) { + // returns all + $result = $this->buffer; + // No more in the buffer + $this->buffer=''; + } + else { + // acquire $length characters from the buffer + $result = substr($this->buffer, 0, $length); + // remove $length characters from the buffer + $this->buffer = substr_replace($this->buffer, '', 0, $length); + } + + return $result; + } + + /** + * Fake write method, do nothing except return the "writen" length + * + * @param string $string The string to write + * @param int|null $length the number of characters to write + * @throws \InvalidArgumentException on invalid length + * @return number of "writen" bytes + */ + public function write(string $string, $length=null) : int + { + if(null === $length) { + $length = strlen($string); + } + + if ($length<0) { + throw new \InvalidArgumentException("Cannot write a negative count of bytes"); + } + + return min($length, strlen($string)); + } + + public function close() + { + $this->buffer = ''; + } +} \ No newline at end of file diff --git a/tests/APIConnectorTest.php b/tests/APIConnectorTest.php new file mode 100644 index 0000000..1e1edc5 --- /dev/null +++ b/tests/APIConnectorTest.php @@ -0,0 +1,92 @@ +assertInstanceOf(APIConnector::class, $apiStream); + if ($closeResource) { + $apiStream->close(); + } + } + + public function constructProvider() + { + return [ + [ new ResourceStream(fopen(__FILE__, 'r')), ], // Myself, sure I exists + [ new ResourceStream(fsockopen('tcp://127.0.0.1', 18728)), ], // Socket + [ new ResourceStream(STDIN), false ], // Try it, but do not close STDIN please !!! + [ new StringStream('Hello World !!!') ], // Try it, but do not close STDIN please !!! + [ new StringStream('') ], // Try it, but do not close STDIN please !!! + // What else ? + ]; + } + + /** + * @covers ::readWord + * @dataProvider readWordProvider + */ + + public function test__readWord(APIConnector $connector, $expected) + { + $this->assertSame($expected, $connector->readWord()); + } + + public function readWordProvider() + { + $longString = '=comment='.str_repeat('a',10000); + $length = strlen($longString); + return [ + [ new APIConnector(new StringStream(chr(0))), ''], + [ new APIConnector(new StringStream(chr(3).'!re')), '!re'], + [ new APIConnector(new StringStream(chr(5).'!done')), '!done'], + [ new APIConnector(new StringStream(APILengthCoDec::encodeLength($length).$longString)), $longString], + ]; + } + + /** + * @covers ::writeWord + * @dataProvider writeWordProvider + */ + public function test_writeWord(APIConnector $connector, string $toWrite, int $expected) + { + $this->assertEquals($expected, $connector->writeWord($toWrite)); + } + + public function writeWordProvider() + { + return [ + [ new APIConnector(new StringStream('Have FUN !!!')), '', 1 ], // length is 0, but have to write it on 1 byte, minimum + [ new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 54), 55 ], // arbitrary value + [ new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 127), 128 ], // maximum value for 1 byte encoding lentgth + [ new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 128), 130 ], // minimum value for 2 bytes encoding lentgth + [ new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 254), 256 ], // special value isn't it ? + [ new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 255), 257 ], // special value isn't it ? + ]; + } +} \ No newline at end of file diff --git a/tests/APILengthCoDecTest.php b/tests/APILengthCoDecTest.php new file mode 100644 index 0000000..69c37e2 --- /dev/null +++ b/tests/APILengthCoDecTest.php @@ -0,0 +1,120 @@ +assertEquals(BinaryStringHelper::IntegerToNBOBinaryString($expected), APILengthCoDec::encodeLength($length)); + } + + public function encodedLengthProvider() + { + // [encoded length value, length value] + return [ + [0, 0], // Low limit value for 1 byte encoded length + [0x39, 0x39], // Arbitrary median value for 1 byte encoded length + [0x7f, 0x7F], // High limit value for 1 byte encoded length + + [0x8080, 0x80], // Low limit value for 2 bytes encoded length + [0x9C42, 0x1C42], // Arbitrary median value for 2 bytes encoded length + [0xBFFF, 0x3FFF], // High limit value for 2 bytes encoded length + + [0xC04000, 0x4000], // Low limit value for 3 bytesv + [0xCAD73B, 0xAD73B], // Arbitrary median value for 3 bytes encoded length + [0xDFFFFF, 0x1FFFFF], // High limit value for 3 bytes encoded length + + [0xE0200000, 0x200000], // Low limit value for 4 bytes encoded length + [0xE5AD736B, 0x5AD736B], // Arbitrary median value for 4 bytes encoded length + [0xEFFFFFFF, 0xFFFFFFF], // High limit value for 4 bytes encoded length + + [0xF010000000, 0x10000000], // Low limit value for 5 bytes encoded length + [0xF10D4EF9C3, 0x10D4EF9C3], // Arbitrary median value for 5 bytes encoded length + [0xF7FFFFFFFF, 0x7FFFFFFFF], // High limit value for 5 bytes encoded length + ]; + } + + /** + * @dataProvider encodedLengthProvider + * @covers ::decodeLength + */ + + public function test__decodeLength($encodedLength, $expected) + { + // We have to provide $encodedLength as a "bytes" stream + $stream = new StringStream(BinaryStringHelper::IntegerToNBOBinaryString($encodedLength)); + $this->assertEquals($expected, APILengthCoDec::decodeLength($stream)); + } + + /** + * @dataProvider decodeLengthControlWordProvider + * @covers ::decodeLength + * @expectedException UnexpectedValueException + */ + public function test_decodeLengthControlWord(string $encodedLength) + { + APILengthCoDec::decodeLength(new StringStream($encodedLength)); + } + + public function decodeLengthControlWordProvider() + { + // Control bytes : 5 most signficants its sets to 1 + return [ + [chr(0xF8)], // minimum + [chr(0xFC)], // arbitraty value + [chr(0xFF)], // maximum + ]; + } +} \ No newline at end of file diff --git a/tests/Helpers/BinaryStringHelperTest.php b/tests/Helpers/BinaryStringHelperTest.php new file mode 100644 index 0000000..2b30b83 --- /dev/null +++ b/tests/Helpers/BinaryStringHelperTest.php @@ -0,0 +1,43 @@ +assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value)); + } + + public function IntegerToNBOBinaryStringProvider() + { + return [ + [0, chr(0)], // lower boundary value + [0xFFFFFFFF, chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)], // 32 bits maximal value + + // strange behaviour : + // TypeError: Argument 1 passed to RouterOS\Tests\Helpers\BinaryStringHelperTest::test__IntegerToNBOBinaryString() must be of the type integer, float given + // Seems that php auto convert to float 0xFFF.... + // + // [0xFFFFFFFFFFFFFFFF, chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)], + + // -1 is encoded with 0xFFFFFFF..... + // 64 bits maximal value (on a 64 bits system) + [-1, chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)], // 64 bits upper boundary value + // Let's try random value + [0x390DDD99, chr(0x39).chr(0x0D).chr(0xDD).chr(0x99)], + + ]; + } +} \ No newline at end of file diff --git a/tests/Streams/ResourceStreamTest.php b/tests/Streams/ResourceStreamTest.php new file mode 100644 index 0000000..9cda9a2 --- /dev/null +++ b/tests/Streams/ResourceStreamTest.php @@ -0,0 +1,246 @@ +getObjectAttribute($resourceStream, 'stream'); + $this->assertInternalType(IsType::TYPE_RESOURCE, $stream); + + if ($closeResource) + { + fclose($resource); + } + } + + /** + * Data provider for test__construct + * + * returns data of type resource + */ + public function constructProvider() + { + return [ + [ fopen(__FILE__, 'r'), ], // Myself, sure I exists + [ fsockopen('tcp://127.0.0.1', 18728), ], // Socket + [ STDIN, false ], // Try it, but do not close STDIN please !!! + // What else ? + ]; + } + + /** + * Test that read function return expected values, and that consecutive reads return data + * + * @covers ::read + * @dataProvider readProvider + * @param resource $resource Cannot typehint, PHP refuse it + * @param string $expected the rsult we should have + */ + public function test__read(ResourceStream $stream, string $expected) + { + $this->assertSame($expected, $stream->read(strlen($expected))); + } + + public function readProvider() + { + $resource = fopen(__FILE__, 'r'); + $me = new ResourceStream($resource); + return [ + [ $me, '<'], // Read for byte + [ $me, '?php'], // Read following bytes. File statrts with "read($length); + } + + public function readBadLengthProvider() + { + $resource = fopen(__FILE__, 'r'); + $me = new ResourceStream($resource); + return [ + [ $me, 0 ], + [ $me, -1 ], + ]; + fclose($resource); + } + /** + * Test read to invalid resource + * + * @covers ::read + * @dataProvider readBadResourceProvider + * @expectedException RouterOS\Exceptions\StreamException + * @param resource $resource Cannot typehint, PHP refuse it + */ + public function test__readBadResource(ResourceStream $stream, int $length) + { + $stream->read($length); + } + + public function readBadResourceProvider() + { + $resource = fopen(__FILE__, 'r'); + $me = new ResourceStream($resource); + fclose($resource); + return [ + [ $me, 1 ], + ]; + } + + /** + * Test that write function returns writen length + * + * @covers ::write + * @dataProvider writeProvider + * @param ResourceStram $resource to test + * @param string $toWrite the writed string + */ + public function test__write(ResourceStream $stream, string $toWrite) + { + $this->assertEquals(strlen($toWrite) , $stream->write($toWrite)); + } + + public function writeProvider() + { + $resource = fopen("/dev/null", 'w'); + $null = new ResourceStream($resource); + return [ + [ $null, 'yyaagagagag'], // Take that + ]; + fclose($resource); + } + + /** + * Test write to invalid resource + * + * @covers ::write + * @dataProvider writeBadResourceProvider + * @expectedException RouterOS\Exceptions\StreamException + * @param resource $resource to test + * @param string $toWrite the writed string + */ + public function test__writeBadResource(ResourceStream $stream, string $toWrite) + { + $stream->write($toWrite); + } + + public function writeBadResourceProvider() + { + $resource = fopen('/dev/null', 'w'); + $me = new ResourceStream($resource); + fclose($resource); + return [ + [ $me, 'sasasaas' ], // Take that + ]; + } + + /** + * Test double close resource + * + * @covers ::close + * @dataProvider doubleCloseProvider + * @expectedException RouterOS\Exceptions\StreamException + * @param resource $resource to test + */ + public function test_doubleClose(ResourceStream $stream) + { + $stream->close(); + $stream->close(); + } + + public function doubleCloseProvider() + { + return [ + [ new ResourceStream(fopen('/dev/null', 'w')), 'sasasaas' ], // Take that + ]; + } + + /** + * Test write to closed resource + * + * @covers ::close + * @covers ::write + * @dataProvider writeClosedResourceProvider + * @expectedException RouterOS\Exceptions\StreamException + * @param resource $resource to test + * @param string $toWrite the writed string + */ + public function test_close(ResourceStream $stream, string $toWrite) + { + $stream->close(); + $stream->write($toWrite); + } + + public function writeClosedResourceProvider() + { + return [ + [ new ResourceStream(fopen('/dev/null', 'w')), 'sasasaas' ], // Take that + ]; + } + +} \ No newline at end of file diff --git a/tests/Streams/StringStreamTest.php b/tests/Streams/StringStreamTest.php new file mode 100644 index 0000000..ebabf89 --- /dev/null +++ b/tests/Streams/StringStreamTest.php @@ -0,0 +1,144 @@ +assertInstanceOf(StringStream::class, new StringStream($string)); + } + + public function constructProvider() + { + return [ + [ chr(0) ], + [ '' ], + [ '1' ], + [ 'lkjl'.chr(0).'kjkljllkjkljljklkjkljlkjljlkjkljkljlkjjll'], + ]; + } + + + /** + * test that write function returns the effective writen bytes + * @covers ::write + * @dataProvider writeProvider + * @param string $toWrite the string to write + * @param int|null $length the count if bytes to write + * @param int $expected the number of bytes that must be writen + */ + + public function test__write(string $string, $length, int $expected) + { + $stream = new StringStream('Does not matters'); + if (is_null($length)) { + $this->assertEquals($expected, $stream->write($string)); + } + else { + $this->assertEquals($expected, $stream->write($string, $length)); + } + + } + + public function writeProvider() + { + return [ + [ '', 0, 0 ], + [ '', 10, 0 ], + [ '', null, 0 ], + [ 'Yabala', 0, 0], + [ 'Yabala', 1, 1], + [ 'Yabala', 6, 6], + [ 'Yabala', 100, 6], + [ 'Yabala', null, 6], + [ chr(0), 0, 0], + [ chr(0), 1, 1], + [ chr(0), 100, 1], + [ chr(0), null, 1], + ]; + } + + /** + * @covers ::write + * @expectedException \InvalidArgumentException + */ + public function test__writeWithNegativeLength() + { + $stream = new StringStream('Does not matters'); + $stream->write("PLOP", -1); + } + + /** + * Test read function + */ + public function test__read() + { + $stream = new StringStream('123456789'); + + $this->assertEquals('', $stream->read(0)); + $this->assertEquals('1', $stream->read(1)); + $this->assertEquals('23', $stream->read(2)); + $this->assertEquals('456', $stream->read(3)); + $this->assertEquals('', $stream->read(0)); + $this->assertEquals('789', $stream->read(4)); + } + + /** + * @expectedException InvalidArgumentException + */ + public function test__readBadLength() + { + $stream = new StringStream('123456789'); + $stream->read(-1); + } + + /** + * @covers ::read + * @dataProvider readWhileEmptyProvider + * @expectedException \RouterOS\Exceptions\StreamException + */ + public function test__readWhileEmpty(StringStream $stream, int $length) + { + $stream->read($length); + } + + public function readWhileEmptyProvider() + { + $stream = new StringStream('123456789'); + $stream->read(9); + yield [$stream, 1]; + + $stream = new StringStream('123456789'); + $stream->read(5); + $stream->read(4); + yield [$stream, 1]; + + $stream = new StringStream(''); + yield [$stream, 1]; + } + + /** + * @expectedException \RouterOS\Exceptions\StreamException + */ + public function testReadClosed() + { + $stream = new StringStream('123456789'); + $stream->close(); + $stream->read(1); + } +} \ No newline at end of file