Browse Source

refactor communication using stream

pull/8/head
Matthieu Racine 7 years ago
parent
commit
a1610f8ccb
  1. 56
      src/APIConnector.php
  2. 197
      src/APILengthCoDec.php
  3. 111
      src/Client.php
  4. 14
      src/Exceptions/StreamException.php
  5. 60
      src/Helpers/BinaryStringHelper.php
  6. 39
      src/Interfaces/StreamInterface.php
  7. 80
      src/Streams/ResourceStream.php
  8. 93
      src/Streams/StringStream.php
  9. 92
      tests/APIConnectorTest.php
  10. 120
      tests/APILengthCoDecTest.php
  11. 43
      tests/Helpers/BinaryStringHelperTest.php
  12. 246
      tests/Streams/ResourceStreamTest.php
  13. 144
      tests/Streams/StringStreamTest.php

56
src/APIConnector.php

@ -0,0 +1,56 @@
<?php
namespace RouterOS;
use RouterOS\Interfaces\StreamInterface;
/**
* class APIConnector
*
* Implement middle level dialog with router by masking word dialog implementation to client class
*
* @package RouterOS
* @since 0.9
*/
class APIConnector
{
/**
* @var StreamInterface $stream The stream used to communicate with the router
*/
protected $stream;
/**
* Constructor
*
* @param StreamInterface $stream
*/
public function __construct(StreamInterface $stream)
{
$this->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);
}
}

197
src/APILengthCoDec.php

@ -0,0 +1,197 @@
<?php
namespace RouterOS;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Helpers\BinaryStringHelper;
/**
* class APILengthCoDec
*
* Coder / Decoder for length field in mikrotik API communication protocol
*
* @package RouterOS
* @since 0.9
*/
class APILengthCoDec
{
public static function encodeLength(int $length)
{
// Encode the length :
// - if length <= 0x7F (binary : 01111111 => 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");
}
}

111
src/Client.php

@ -46,6 +46,14 @@ class Client implements Interfaces\ClientInterface
private $_config; private $_config;
/** /**
* API communication object
*
* @var APIConnector
*/
private $connector;
/**
* Client constructor. * Client constructor.
* *
* @param array|\RouterOS\Config $config * @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) * Send write query to RouterOS (with or without tag)
* *
* @param string|array|\RouterOS\Query $query * @param string|array|\RouterOS\Query $query
@ -211,12 +139,11 @@ class Client implements Interfaces\ClientInterface
// Send commands via loop to router // Send commands via loop to router
foreach ($query->getQuery() as $command) { 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; return $this;
} }
@ -242,12 +169,9 @@ class Client implements Interfaces\ClientInterface
// Read answer from socket in loop // Read answer from socket in loop
while (true) { 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) { if ($lastReply) {
// We received a !done or !fatal message in a precedent loop // We received a !done or !fatal message in a precedent loop
// response is complete // response is complete
@ -260,12 +184,12 @@ class Client implements Interfaces\ClientInterface
} }
// Save output line to response array // 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 // 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 // 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 socket is active
if (null !== $this->getSocket()) { if (null !== $this->getSocket()) {
$this->connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
// If we logged in then exit from loop // If we logged in then exit from loop
if (true === $this->login()) { if (true === $this->login()) {
$connected = true; $connected = true;
@ -458,6 +382,7 @@ class Client implements Interfaces\ClientInterface
// Else close socket and start from begin // Else close socket and start from begin
$this->closeSocket(); $this->closeSocket();
$this->stream = null;
} }
// Sleep some time between tries // Sleep some time between tries

14
src/Exceptions/StreamException.php

@ -0,0 +1,14 @@
<?php
namespace RouterOS\Exceptions;
/**
* Class StreamException
*
* @package RouterOS\Exceptions
* @since 0.9
*/
class StreamException extends \Exception
{
}

60
src/Helpers/BinaryStringHelper.php

@ -0,0 +1,60 @@
<?php
namespace RouterOS\Helpers;
/**
* class BinaryStringHelper
*
* Strings and binary datas manipulations
*
* @package RouterOS\Helpers
* @since 0.9
*/
class BinaryStringHelper
{
/**
* Convert an integer value in a "Network Byte Ordered" binary string (most significant value first)
*
* Reads the integer, starting from the most signficant byte, one byte a time.
* Once reach a non 0 byte, construct a binary string representing this values
* ex :
* 0xFF7 => 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;
}
}

39
src/Interfaces/StreamInterface.php

@ -0,0 +1,39 @@
<?php
namespace RouterOS\Interfaces;
/**
* Interface QueryInterface
*
* Stream abstraction
*
* @package RouterOS\Interfaces
* @since 0.9
*/
interface StreamInterface
{
/**
* Reads a stream
*
* Reads $length bytes from the stream, returns the bytes into a string
* Must be binary safe (as fread).
*
* @param int $length the numer of bytes to read
* @return string a binary string containing the readed byes
*/
public function read(int $length) : string;
/**
* Writes a string to a stream
*
* Write $length bytes of string, if not mentioned, write all the string
* Must be binary safe (as fread).
* if $length is greater than string length, write all string and return number of writen bytes
* if $length os smaller than string length, remaining bytes are losts.
*
* @param int $length the numer of bytes to read
* @return int the numer of writen bytes
*/
public function write(string $string, $length=-1) : int;
public function close();
}

80
src/Streams/ResourceStream.php

@ -0,0 +1,80 @@
<?php
namespace RouterOS\Streams;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Exceptions\StreamException;
/**
* class ResourceStream
*
* Stream using a resource (socket, file, pipe etc.)
*
* @package RouterOS
* @since 0.9
*/
class ResourceStream implements StreamInterface
{
protected $stream;
public function __construct($stream)
{
if (false === is_resource($stream)) {
throw new \InvalidArgumentException(
sprintf(
'Argument must be a valid resource type. %s given.',
gettype($stream)
)
);
}
// // TODO : Should we verify the resource type ?
$this->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");
}
}
}

93
src/Streams/StringStream.php

@ -0,0 +1,93 @@
<?php
namespace RouterOS\Streams;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Exceptions\StreamException;
/**
* class StringStream
*
* Initialized with a string, the read method retreive it as done with fread, consuming the buffer.
* When all the string has been read, exception is thrown when try to read again.
*
* @package RouterOS\Streams
* @since 0.9
*/
class StringStream implements StreamInterface
{
/**
* @var string $buffer Stores the string to use
*/
protected $buffer;
/**
* Constuctor
*
* @param string $string
*/
public function __construct(string $string)
{
$this->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 = '';
}
}

92
tests/APIConnectorTest.php

@ -0,0 +1,92 @@
<?php
namespace RouterOS\Tests;
use PHPUnit\Framework\TestCase;
use RouterOS\APIConnector;
use RouterOS\Streams\StringStream;
use RouterOS\Streams\ResourceStream;
use RouterOS\APILengthCoDec;
use RouterOS\Interfaces\StreamInterface;
/**
* Limit code coverage to the class RouterOS\APIStream
* @coversDefaultClass RouterOS\APIConnector
*/
class APIConnectorTest extends TestCase
{
/**
* Test that constructor is OK with different kinds of resources
*
* @covers ::__construct
* @dataProvider constructProvider
* @param Resource $resource Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ?
*/
public function test_construct(StreamInterface $stream, bool $closeResource=false)
{
$apiStream = new APIConnector($stream);
$this->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 ?
];
}
}

120
tests/APILengthCoDecTest.php

@ -0,0 +1,120 @@
<?php
namespace RouterOS\Tests;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\APILengthCoDec;
use RouterOS\Streams\StringStream;
use RouterOS\Helpers\BinaryStringHelper;
/**
* Limit code coverage to the class
* @coversDefaultClass RouterOS\APILengthCoDec
*/
class APILengthCoDecTest extends TestCase
{
/**
* @dataProvider encodeLengthNegativeProvider
* @expectedException \DomainException
* @covers ::encodeLength
*/
public function test__encodeLengthNegative($length)
{
APILengthCoDec::encodeLength($length);
}
public function encodeLengthNegativeProvider()
{
return [
[-1],
[PHP_INT_MIN],
];
}
/**
* @dataProvider encodeLengthTooLargeProvider
* @expectedException \DomainException
* @covers ::encodeLength
*/
public function test__encodeLengthTooLarge($length)
{
APILengthCoDec::encodeLength($length);
}
public function encodeLengthTooLargeProvider()
{
return [
[0x7FFFFFFFFF+1],
[PHP_INT_MAX],
];
}
/**
* @dataProvider encodedLengthProvider
* @covers ::encodeLength
*/
public function test__encodeLength($expected, $length)
{
$this->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
];
}
}

43
tests/Helpers/BinaryStringHelperTest.php

@ -0,0 +1,43 @@
<?php
namespace RouterOS\Tests\Helpers;
use PHPUnit\Framework\TestCase;
use RouterOS\Helpers\BinaryStringHelper;
/**
* Limit code coverage to the class
* @coversDefaultClass RouterOS\Helpers\BinaryStringHelper
*/
class BinaryStringHelperTest extends TestCase
{
/**
* @dataProvider IntegerToNBOBinaryStringProvider
* @covers ::IntegerToNBOBinaryString
*/
public function test__IntegerToNBOBinaryString(int $value, string $expected)
{
$this->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)],
];
}
}

246
tests/Streams/ResourceStreamTest.php

@ -0,0 +1,246 @@
<?php
namespace RouterOS\Tests\Streams;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\Streams\ResourceStream;
use RouterOS\Exceptions\StreamException;
/**
* Limit code coverage to the class RouterOS\APIStream
* @coversDefaultClass RouterOS\Streams\ResourceStream
*/
class ResourceStreamTest extends TestCase
{
/**
* Test that constructor throws an InvalidArgumentException on bad parameter type
*
* @covers ::__construct
* @expectedException \InvalidArgumentException
* @dataProvider constructNotResourceProvider
*/
public function test__constructNotResource($notResource)
{
new ResourceStream($notResource);
}
/**
* Data provider for test__constructNotResource
*
* returns data not of type resource
*/
public function constructNotResourceProvider()
{
return [
[0], // integer
[3.14], // float
['a string'], // string
[
[ 0 , 3.14 ] // Array
],
[ new \stdClass() ], // Object
// What else ?
];
}
/**
* Test that constructor is OK with different kinds of resources
*
* @covers ::__construct
* @dataProvider constructProvider
* @param resource $resource Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ?
*/
public function test_construct($resource, bool $closeResource=true)
{
$resourceStream = new ResourceStream($resource);
$stream = $this->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 "<php"
];
fclose($resource);
}
/**
* Test that read invalid lengths
*
* @covers ::read
* @dataProvider readBadLengthProvider
* @expectedException \InvalidArgumentException
* @param resource $resource Cannot typehint, PHP refuse it
*/
public function test__readBadLength(ResourceStream $stream, int $length)
{
$stream->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
];
}
}

144
tests/Streams/StringStreamTest.php

@ -0,0 +1,144 @@
<?php
namespace RouterOS\Tests\Streams;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\Streams\StringStream;
use RouterOS\Exceptions\StreamException;
/**
* Limit code coverage to the class RouterOS\APIStream
* @coversDefaultClass RouterOS\Streams\StringStream
*/
class StringStreamTest extends TestCase
{
/**
* @covers ::__construct
* @dataProvider constructProvider
*/
public function test__construct(string $string)
{
$this->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);
}
}
Loading…
Cancel
Save