Browse Source
Merge pull request #8 from EvilFreelancer/streamrefactor
Merge pull request #8 from EvilFreelancer/streamrefactor
Stream refactoringtags/0.9 0.9
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1332 additions and 140 deletions
-
7phpunit.xml
-
59src/APIConnector.php
-
196src/APILengthCoDec.php
-
127src/Client.php
-
14src/Exceptions/StreamException.php
-
65src/Helpers/BinaryStringHelper.php
-
46src/Interfaces/StreamInterface.php
-
21src/SocketTrait.php
-
110src/Streams/ResourceStream.php
-
97src/Streams/StringStream.php
-
98tests/APIConnectorTest.php
-
106tests/APILengthCoDecTest.php
-
46tests/ClientTest.php
-
49tests/Helpers/BinaryStringHelperTest.php
-
263tests/Streams/ResourceStreamTest.php
-
160tests/Streams/StringStreamTest.php
@ -0,0 +1,59 @@ |
|||
<?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); |
|||
return ($length > 0) ? $this->stream->read($length) : ''; |
|||
} |
|||
|
|||
/** |
|||
* Write word to stream |
|||
* |
|||
* @param string $word |
|||
* @return int return number of written bytes |
|||
*/ |
|||
public function writeWord(string $word): int |
|||
{ |
|||
$encodedLength = APILengthCoDec::encodeLength(strlen($word)); |
|||
return $this->stream->write($encodedLength . $word); |
|||
} |
|||
} |
|||
@ -0,0 +1,196 @@ |
|||
<?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 |
|||
{ |
|||
/** |
|||
* Encode string to length of string |
|||
* |
|||
* @param int|float $length |
|||
* @return string |
|||
*/ |
|||
public static function encodeLength($length): string |
|||
{ |
|||
// 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 significance 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 significance 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 significance 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 significance bits (11110)
|
|||
// - end
|
|||
// - length > 0x7FFFFFFFFF : not supported
|
|||
|
|||
if ($length < 0) { |
|||
throw new \DomainException("Length of word could not to be negative ($length)"); |
|||
} |
|||
|
|||
if ($length <= 0x7F) { |
|||
return BinaryStringHelper::IntegerToNBOBinaryString($length); |
|||
} |
|||
|
|||
if ($length <= 0x3FFF) { |
|||
return BinaryStringHelper::IntegerToNBOBinaryString(0x8000 + $length); |
|||
} |
|||
|
|||
if ($length <= 0x1FFFFF) { |
|||
return BinaryStringHelper::IntegerToNBOBinaryString(0xC00000 + $length); |
|||
} |
|||
|
|||
if ($length <= 0x0FFFFFFF) { |
|||
return BinaryStringHelper::IntegerToNBOBinaryString(0xE0000000 + $length); |
|||
} |
|||
|
|||
// https://wiki.mikrotik.com/wiki/Manual:API#API_words
|
|||
// If len >= 0x10000000 then 0xF0 and len as four bytes
|
|||
return BinaryStringHelper::IntegerToNBOBinaryString(0xF000000000 + $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): int |
|||
{ |
|||
// 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 significands bits to 0
|
|||
$result = $firstByte & 0x3F; |
|||
|
|||
// shift left 8 bits to have 2 bytes
|
|||
$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 significands bits to 0
|
|||
$result = $firstByte & 0x1F; |
|||
|
|||
// shift left 16 bits to have 3 bytes
|
|||
$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 significance bits to 0
|
|||
$result = $firstByte & 0x0F; |
|||
|
|||
// shift left 24 bits to have 4 bytes
|
|||
$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 possible 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("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system"); |
|||
// @codeCoverageIgnoreEnd
|
|||
} |
|||
|
|||
// Set 5 most significance bits to 0
|
|||
$result = $firstByte & 0x07; |
|||
|
|||
// shift left 232 bits to have 5 bytes
|
|||
$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 significance bits are set to 1 (11111xxx)
|
|||
// This is a control word, not implemented by Mikrotik for the moment
|
|||
throw new \UnexpectedValueException('Control Word found'); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<?php |
|||
|
|||
namespace RouterOS\Exceptions; |
|||
|
|||
/** |
|||
* Class StreamException |
|||
* |
|||
* @package RouterOS\Exceptions |
|||
* @since 0.9 |
|||
*/ |
|||
|
|||
class StreamException extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
<?php |
|||
|
|||
namespace RouterOS\Helpers; |
|||
|
|||
/** |
|||
* class BinaryStringHelper |
|||
* |
|||
* Strings and binary data 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 significant 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|float $value the integer value to be converted |
|||
* @return string the binary string |
|||
*/ |
|||
public static function IntegerToNBOBinaryString($value): string |
|||
{ |
|||
// 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) || $buffer !== '') { |
|||
// Get the current 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,46 @@ |
|||
<?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 string $string |
|||
* @param int $length the number of bytes to read |
|||
* @return int return number of written bytes |
|||
*/ |
|||
public function write(string $string, int $length = -1): int; |
|||
|
|||
/** |
|||
* Close stream connection |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function close(); |
|||
} |
|||
@ -0,0 +1,110 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* ResourceStream constructor. |
|||
* |
|||
* @param $stream |
|||
*/ |
|||
public function __construct($stream) |
|||
{ |
|||
if (!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; |
|||
} |
|||
|
|||
/** |
|||
* @param int $length |
|||
* @return string |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
* @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'); |
|||
} |
|||
|
|||
// TODO: Ignore errors here, but why?
|
|||
$result = @fread($this->stream, $length); |
|||
|
|||
if (false === $result) { |
|||
throw new StreamException("Error reading $length bytes"); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* 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 string $string |
|||
* @param int|null $length the numer of bytes to read |
|||
* @return int the number of written bytes |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
public function write(string $string, int $length = null): int |
|||
{ |
|||
if (null === $length) { |
|||
$length = strlen($string); |
|||
} |
|||
|
|||
// TODO: Ignore errors here, but why?
|
|||
$result = @fwrite($this->stream, $string, $length); |
|||
|
|||
if (false === $result) { |
|||
throw new StreamException("Error writing $length bytes"); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Close stream connection |
|||
* |
|||
* @return void |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$hasBeenClosed = false; |
|||
|
|||
if (null !== $this->stream) { |
|||
$hasBeenClosed = @fclose($this->stream); |
|||
$this->stream = null; |
|||
} |
|||
|
|||
if (false === $hasBeenClosed) { |
|||
throw new StreamException('Error closing stream'); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<?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; |
|||
|
|||
/** |
|||
* StringStream constructor. |
|||
* |
|||
* @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 |
|||
* @return int number of "writen" bytes |
|||
* @throws \InvalidArgumentException on invalid length |
|||
*/ |
|||
public function write(string $string, int $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)); |
|||
} |
|||
|
|||
/** |
|||
* Close stream connection |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function close() |
|||
{ |
|||
$this->buffer = ''; |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
<?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 StreamInterface $stream 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(): array |
|||
{ |
|||
return [ |
|||
[new ResourceStream(fopen(__FILE__, 'rb')),], // Myself, sure I exists
|
|||
[new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN'))),], // 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 |
|||
* |
|||
* @param APIConnector $connector |
|||
* @param string $expected |
|||
*/ |
|||
public function test__readWord(APIConnector $connector, string $expected) |
|||
{ |
|||
$this->assertSame($expected, $connector->readWord()); |
|||
} |
|||
|
|||
public function readWordProvider(): array |
|||
{ |
|||
$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 |
|||
* |
|||
* @param APIConnector $connector |
|||
* @param string $toWrite |
|||
* @param int $expected |
|||
*/ |
|||
public function test_writeWord(APIConnector $connector, string $toWrite, int $expected) |
|||
{ |
|||
$this->assertEquals($expected, $connector->writeWord($toWrite)); |
|||
} |
|||
|
|||
public function writeWordProvider(): array |
|||
{ |
|||
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,106 @@ |
|||
<?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(): array |
|||
{ |
|||
return [ |
|||
[-1], |
|||
[PHP_INT_MIN], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider encodedLengthProvider |
|||
* @covers ::encodeLength |
|||
*/ |
|||
public function test__encodeLength($expected, $length) |
|||
{ |
|||
$this->assertEquals(BinaryStringHelper::IntegerToNBOBinaryString((int) $expected), APILengthCoDec::encodeLength($length)); |
|||
} |
|||
|
|||
public function encodedLengthProvider(): array |
|||
{ |
|||
// [encoded length value, length value]
|
|||
$result = [ |
|||
[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 bytes
|
|||
[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
|
|||
]; |
|||
|
|||
if (PHP_INT_SIZE > 4) { |
|||
$result[] = [0xF010000000, 0x10000000]; // Low limit value for 5 bytes encoded length
|
|||
$result[] = [0xF10D4EF9C3, 0x10D4EF9C3]; // Arbitrary median value for 5 bytes encoded length
|
|||
$result[] = [0xF7FFFFFFFF, 0x7FFFFFFFF]; // High limit value for 5 bytes encoded length
|
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* @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(): array |
|||
{ |
|||
// Control bytes: 5 most significance its sets to 1
|
|||
return [ |
|||
[chr(0xF8)], // minimum
|
|||
[chr(0xFC)], // arbitrary value
|
|||
[chr(0xFF)], // maximum
|
|||
]; |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
<?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($value, $expected) |
|||
{ |
|||
$this->assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value)); |
|||
} |
|||
|
|||
public function IntegerToNBOBinaryStringProvider(): array |
|||
{ |
|||
$result = [ |
|||
[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)],
|
|||
|
|||
// Let's try random value
|
|||
[0x390DDD99, chr(0x39) . chr(0x0D) . chr(0xDD) . chr(0x99)], |
|||
]; |
|||
|
|||
if (PHP_INT_SIZE > 4) { |
|||
// -1 is encoded with 0xFFFFFFF.....
|
|||
// 64 bits maximal value (on a 64 bits system only)
|
|||
$result[] = [-1, chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF)]; // 64 bits upper boundary value
|
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,263 @@ |
|||
<?php |
|||
|
|||
namespace RouterOS\Tests\Streams; |
|||
|
|||
use PHPUnit\Framework\TestCase; |
|||
use PHPUnit\Framework\Constraint\IsType; |
|||
use RouterOS\Streams\ResourceStream; |
|||
|
|||
/** |
|||
* 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 |
|||
* |
|||
* @param $notResource |
|||
*/ |
|||
|
|||
public function test__constructNotResource($notResource) |
|||
{ |
|||
new ResourceStream($notResource); |
|||
} |
|||
|
|||
/** |
|||
* Data provider for test__constructNotResource |
|||
* |
|||
* returns data not of type resource |
|||
*/ |
|||
public function constructNotResourceProvider(): array |
|||
{ |
|||
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 |
|||
* |
|||
* @return array data of type resource |
|||
*/ |
|||
public function constructProvider(): array |
|||
{ |
|||
return [ |
|||
[fopen(__FILE__, 'rb'),], // Myself, sure I exists
|
|||
[fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN')),], // 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 ResourceStream $stream Cannot typehint, PHP refuse it |
|||
* @param string $expected the result we should have |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function test__read(ResourceStream $stream, string $expected) |
|||
{ |
|||
$this->assertSame($expected, $stream->read(strlen($expected))); |
|||
} |
|||
|
|||
public function readProvider(): array |
|||
{ |
|||
$resource = fopen(__FILE__, 'rb'); |
|||
$me = new ResourceStream($resource); |
|||
|
|||
return [ |
|||
[$me, '<'], // Read for byte
|
|||
[$me, '?php'], // Read following bytes. File statrts with "<php"
|
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test that read invalid lengths |
|||
* |
|||
* @covers ::read |
|||
* @dataProvider readBadLengthProvider |
|||
* @expectedException \InvalidArgumentException |
|||
* |
|||
* @param ResourceStream $stream Cannot typehint, PHP refuse it |
|||
* @param int $length |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
* @throws \InvalidArgumentException |
|||
*/ |
|||
public function test__readBadLength(ResourceStream $stream, int $length) |
|||
{ |
|||
$stream->read($length); |
|||
} |
|||
|
|||
public function readBadLengthProvider(): array |
|||
{ |
|||
$resource = fopen(__FILE__, 'rb'); |
|||
$me = new ResourceStream($resource); |
|||
|
|||
return [ |
|||
[$me, 0], |
|||
[$me, -1], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test read to invalid resource |
|||
* |
|||
* @covers ::read |
|||
* @dataProvider readBadResourceProvider |
|||
* @expectedException \RouterOS\Exceptions\StreamException |
|||
* |
|||
* @param ResourceStream $stream Cannot typehint, PHP refuse it |
|||
* @param int $length |
|||
*/ |
|||
public function test__readBadResource(ResourceStream $stream, int $length) |
|||
{ |
|||
$stream->read($length); |
|||
} |
|||
|
|||
public function readBadResourceProvider(): array |
|||
{ |
|||
$resource = fopen(__FILE__, 'rb'); |
|||
$me = new ResourceStream($resource); |
|||
fclose($resource); |
|||
return [ |
|||
[$me, 1], |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test that write function returns writen length |
|||
* |
|||
* @covers ::write |
|||
* @dataProvider writeProvider |
|||
* |
|||
* @param ResourceStream $stream to test |
|||
* @param string $toWrite the writed string |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
public function test__write(ResourceStream $stream, string $toWrite) |
|||
{ |
|||
$this->assertEquals(strlen($toWrite), $stream->write($toWrite)); |
|||
} |
|||
|
|||
public function writeProvider(): array |
|||
{ |
|||
$resource = fopen('/dev/null', 'wb'); |
|||
$null = new ResourceStream($resource); |
|||
|
|||
return [ |
|||
[$null, 'yyaagagagag'], // Take that
|
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test write to invalid resource |
|||
* |
|||
* @covers ::write |
|||
* @dataProvider writeBadResourceProvider |
|||
* @expectedException \RouterOS\Exceptions\StreamException |
|||
* |
|||
* @param ResourceStream $stream to test |
|||
* @param string $toWrite the written string |
|||
*/ |
|||
public function test__writeBadResource(ResourceStream $stream, string $toWrite) |
|||
{ |
|||
$stream->write($toWrite); |
|||
} |
|||
|
|||
public function writeBadResourceProvider(): array |
|||
{ |
|||
$resource = fopen('/dev/null', 'wb'); |
|||
$me = new ResourceStream($resource); |
|||
fclose($resource); |
|||
|
|||
return [ |
|||
[$me, 'sasasaas'], // Take that
|
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test double close resource |
|||
* |
|||
* @covers ::close |
|||
* @dataProvider doubleCloseProvider |
|||
* @expectedException \RouterOS\Exceptions\StreamException |
|||
* |
|||
* @param ResourceStream $stream to test |
|||
*/ |
|||
public function test_doubleClose(ResourceStream $stream) |
|||
{ |
|||
$stream->close(); |
|||
$stream->close(); |
|||
} |
|||
|
|||
public function doubleCloseProvider(): array |
|||
{ |
|||
return [ |
|||
[new ResourceStream(fopen('/dev/null', 'wb')), 'sasasaas'], // Take that
|
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* Test write to closed resource |
|||
* |
|||
* @covers ::close |
|||
* @covers ::write |
|||
* @dataProvider writeClosedResourceProvider |
|||
* @expectedException \RouterOS\Exceptions\StreamException |
|||
* |
|||
* @param ResourceStream $stream to test |
|||
* @param string $toWrite the written string |
|||
*/ |
|||
public function test_close(ResourceStream $stream, string $toWrite) |
|||
{ |
|||
$stream->close(); |
|||
$stream->write($toWrite); |
|||
} |
|||
|
|||
public function writeClosedResourceProvider(): array |
|||
{ |
|||
return [ |
|||
[new ResourceStream(fopen('/dev/null', 'wb')), 'sasasaas'], // Take that
|
|||
]; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
<?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 |
|||
* |
|||
* @param string $string |
|||
*/ |
|||
public function test__construct(string $string) |
|||
{ |
|||
$this->assertInstanceOf(StringStream::class, new StringStream($string)); |
|||
} |
|||
|
|||
public function constructProvider(): array |
|||
{ |
|||
return [ |
|||
[chr(0)], |
|||
[''], |
|||
['1'], |
|||
['lkjl' . chr(0) . 'kjkljllkjkljljklkjkljlkjljlkjkljkljlkjjll'], |
|||
]; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* Test that write function returns the effective written bytes |
|||
* |
|||
* @covers ::write |
|||
* @dataProvider writeProvider |
|||
* |
|||
* @param string $string 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 (null === $length) { |
|||
$this->assertEquals($expected, $stream->write($string)); |
|||
} else { |
|||
$this->assertEquals($expected, $stream->write($string, $length)); |
|||
} |
|||
|
|||
} |
|||
|
|||
public function writeProvider(): array |
|||
{ |
|||
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
|
|||
* |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
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 |
|||
* |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
public function test__readBadLength() |
|||
{ |
|||
$stream = new StringStream('123456789'); |
|||
$stream->read(-1); |
|||
} |
|||
|
|||
/** |
|||
* @covers ::read |
|||
* @dataProvider readWhileEmptyProvider |
|||
* @expectedException \RouterOS\Exceptions\StreamException |
|||
* |
|||
* @param StringStream $stream |
|||
* @param int $length |
|||
* @throws \RouterOS\Exceptions\StreamException |
|||
*/ |
|||
public function test__readWhileEmpty(StringStream $stream, int $length) |
|||
{ |
|||
$stream->read($length); |
|||
} |
|||
|
|||
/** |
|||
* @return \Generator |
|||
* @throws StreamException |
|||
*/ |
|||
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