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
-
131src/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
-
50tests/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