13 changed files with 1202 additions and 93 deletions
-
56src/APIConnector.php
-
197src/APILengthCoDec.php
-
111src/Client.php
-
14src/Exceptions/StreamException.php
-
60src/Helpers/BinaryStringHelper.php
-
39src/Interfaces/StreamInterface.php
-
80src/Streams/ResourceStream.php
-
93src/Streams/StringStream.php
-
92tests/APIConnectorTest.php
-
120tests/APILengthCoDecTest.php
-
43tests/Helpers/BinaryStringHelperTest.php
-
246tests/Streams/ResourceStreamTest.php
-
144tests/Streams/StringStreamTest.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); |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<?php |
|||
|
|||
namespace RouterOS\Exceptions; |
|||
|
|||
/** |
|||
* Class StreamException |
|||
* |
|||
* @package RouterOS\Exceptions |
|||
* @since 0.9 |
|||
*/ |
|||
|
|||
class StreamException extends \Exception |
|||
{ |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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"); |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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 = ''; |
|||
} |
|||
} |
|||
@ -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 ?
|
|||
]; |
|||
} |
|||
} |
|||
@ -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
|
|||
]; |
|||
} |
|||
} |
|||
@ -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)], |
|||
|
|||
]; |
|||
} |
|||
} |
|||
@ -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
|
|||
]; |
|||
} |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue