Completed
Push — develop ( a93c21...24c2e3 )
by Vasil
09:35
created

Communicator::_sendWord()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 2
1
<?php
2
3
/**
4
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Id$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
/**
19
 * The namespace declaration.
20
 */
21
namespace PEAR2\Net\RouterOS;
22
23
/**
24
 * Using transmitters.
25
 */
26
use PEAR2\Net\Transmitter as T;
27
28
/**
29
 * A RouterOS communicator.
30
 *
31
 * Implementation of the RouterOS API protocol. Unlike the other classes in this
32
 * package, this class doesn't provide any conveniences beyond the low level
33
 * implementation details (automatic word length encoding/decoding, charset
34
 * translation and data integrity), and because of that, its direct usage is
35
 * strongly discouraged.
36
 *
37
 * @category Net
38
 * @package  PEAR2_Net_RouterOS
39
 * @author   Vasil Rangelov <[email protected]>
40
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
41
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
42
 * @see      Client
43
 */
44
class Communicator
45
{
46
    /**
47
     * Used when getting/setting all (default) charsets.
48
     */
49
    const CHARSET_ALL = -1;
50
51
    /**
52
     * Used when getting/setting the (default) remote charset.
53
     *
54
     * The remote charset is the charset in which RouterOS stores its data.
55
     * If you want to keep compatibility with your Winbox, this charset should
56
     * match the default charset from your Windows' regional settings.
57
     */
58
    const CHARSET_REMOTE = 0;
59
60
    /**
61
     * Used when getting/setting the (default) local charset.
62
     *
63
     * The local charset is the charset in which the data from RouterOS will be
64
     * returned as. This charset should match the charset of the place the data
65
     * will eventually be written to.
66
     */
67
    const CHARSET_LOCAL = 1;
68
69
    /**
70
     * An array with the default charset.
71
     *
72
     * Charset types as keys, and the default charsets as values.
73
     *
74
     * @var array<string,string|null>
75
     */
76
    protected static $defaultCharsets = array(
77
        self::CHARSET_REMOTE => null,
78
        self::CHARSET_LOCAL  => null
79
    );
80
81
    /**
82
     * An array with the current charset.
83
     *
84
     * Charset types as keys, and the current charsets as values.
85
     *
86
     * @var array<string,string|null>
87
     */
88
    protected $charsets = array();
89
90
    /**
91
     * Length of next word.
92
     *
93
     * Length of next word. NULL if no peeking was done.
94
     *
95
     * @var int|double|null
96
     */
97
    protected $nextWordLength = null;
98
99
    /**
100
     * Last state of the word lock.
101
     *
102
     * Last state of the word lock when using persisntent connections.
103
     * Unused by non-persistent connections.
104
     *
105
     * @var int|false
106
     */
107
    protected $nextWordLock = false;
108
109
    /**
110
     * The transmitter for the connection.
111
     *
112
     * @var T\TcpClient
113
     */
114
    protected $trans;
115
116
    /**
117
     * Creates a new connection with the specified options.
118
     *
119
     * @param string        $host    Hostname (IP or domain) of RouterOS.
120
     * @param int|null      $port    The port on which the RouterOS host
121
     *     provides the API service. You can also specify NULL, in which case
122
     *     the port will automatically be chosen between 8728 and 8729,
123
     *     depending on the value of $crypto.
124
     * @param bool          $persist Whether or not the connection should be a
125
     *     persistent one.
126
     * @param double|null   $timeout The timeout for the connection.
127
     * @param string        $key     A string that uniquely identifies the
128
     *     connection.
129
     * @param string        $crypto  The encryption for this connection.
130
     *     Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
131
     *     constants. Off by default. RouterOS currently supports only TLS, but
132
     *     the setting is provided in this fashion for forward compatibility's
133
     *     sake. And for the sake of simplicity, if you specify an encryption,
134
     *     don't specify a context and your default context uses the value
135
     *     "DEFAULT" for ciphers, "ADH" will be automatically added to the list
136
     *     of ciphers.
137
     * @param resource|null $context A context for the socket.
138
     *
139
     * @see sendWord()
140
     */
141
    public function __construct(
142
        $host,
143
        $port = 8728,
144
        $persist = false,
145
        $timeout = null,
146
        $key = '',
147
        $crypto = T\NetworkStream::CRYPTO_OFF,
148
        $context = null
149
    ) {
150
        $isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
151
        if (($context === null) && !$isUnencrypted) {
152
            $context = stream_context_get_default();
153
            $opts = stream_context_get_options($context);
154
            if (!isset($opts['ssl']['ciphers'])
155
                || 'DEFAULT' === $opts['ssl']['ciphers']
156
            ) {
157
                stream_context_set_option(
158
                    $context,
159
                    array(
160
                        'ssl' => array(
161
                            'ciphers' => 'ADH',
162
                            'verify_peer' => false,
163
                            'verify_peer_name' => false
164
                        )
165
                    )
166
                );
167
            }
168
        }
169
        // @codeCoverageIgnoreStart
170
        // The $port is customizable in testing.
171
        if (null === $port) {
172
            $port = $isUnencrypted ? 8728 : 8729;
173
        }
174
        // @codeCoverageIgnoreEnd
175
176
        try {
177
            $this->trans = new T\TcpClient(
178
                $host,
179
                $port,
180
                $persist,
181
                $timeout,
182
                $key,
183
                $crypto,
184
                $context
185
            );
186
        } catch (T\Exception $e) {
187
            throw new SocketException(
188
                'Error connecting to RouterOS',
189
                SocketException::CODE_CONNECTION_FAIL,
190
                $e
191
            );
192
        }
193
        $this->setCharset(
194
            self::getDefaultCharset(self::CHARSET_ALL),
195
            self::CHARSET_ALL
196
        );
197
    }
198
199
    /**
200
     * A shorthand gateway.
201
     *
202
     * This is a magic PHP method that allows you to call the object as a
203
     * function. Depending on the argument given, one of the other functions in
204
     * the class is invoked and its returned value is returned by this function.
205
     *
206
     * @param string|null $string A string of the word to send, or NULL to get
207
     *     the next word as a string.
208
     *
209
     * @return int|string If a string is provided, returns the number of bytes
210
     *     sent, otherwise returns the next word as a string.
211
     */
212
    public function __invoke($string = null)
213
    {
214
        return null === $string ? $this->getNextWord()
215
            : $this->sendWord($string);
216
    }
217
218
    /**
219
     * Checks whether a variable is a seekable stream resource.
220
     *
221
     * @param mixed $var The value to check.
222
     *
223
     * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
224
     */
225
    public static function isSeekableStream($var)
226
    {
227
        if (T\Stream::isStream($var)) {
228
            $meta = stream_get_meta_data($var);
229
            return $meta['seekable'];
230
        }
231
        return false;
232
    }
233
234
    /**
235
     * Uses iconv to convert a stream from one charset to another.
236
     *
237
     * @param string   $inCharset  The charset of the stream.
238
     * @param string   $outCharset The desired resulting charset.
239
     * @param resource $stream     The stream to convert. The stream is assumed
240
     *     to be seekable, and is read from its current position to its end,
241
     *     after which, it is seeked back to its starting position.
242
     *
243
     * @return resource A new stream that uses the $out_charset. The stream is a
244
     *     subset from the original stream, from its current position to its
245
     *     end, seeked at its start.
246
     */
247
    public static function iconvStream($inCharset, $outCharset, $stream)
248
    {
249
        $bytes = 0;
250
        $result = fopen('php://temp', 'r+b');
251
        $iconvFilter = stream_filter_append(
252
            $result,
253
            'convert.iconv.' . $inCharset . '.' . $outCharset,
254
            STREAM_FILTER_WRITE
255
        );
256
257
        flock($stream, LOCK_SH);
258
        $reader = new T\Stream($stream, false);
259
        $writer = new T\Stream($result, false);
260
        $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
261
        while ($reader->isAvailable() && $reader->isDataAwaiting()) {
262
            $bytes += $writer->send(fread($stream, $chunkSize));
263
        }
264
        fseek($stream, -$bytes, SEEK_CUR);
265
        flock($stream, LOCK_UN);
266
267
        stream_filter_remove($iconvFilter);
268
        rewind($result);
269
        return $result;
270
    }
271
272
    /**
273
     * Sets the default charset(s) for new connections.
274
     *
275
     * @param mixed $charset     The charset to set. If $charsetType is
276
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
277
     *     all charsets, or an array with the charset types as keys, and the
278
     *     charsets as values.
279
     * @param int   $charsetType Which charset to set. Valid values are the
280
     *     CHARSET_* constants. Any other value is treated as
281
     *     {@link self::CHARSET_ALL}.
282
     *
283
     * @return string|array The old charset. If $charsetType is
284
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
285
     *     array with the types as keys, and charsets as values.
286
     *
287
     * @see setCharset()
288
     */
289
    public static function setDefaultCharset(
290
        $charset,
291
        $charsetType = self::CHARSET_ALL
292
    ) {
293
        if (array_key_exists($charsetType, self::$defaultCharsets)) {
294
             $oldCharset = self::$defaultCharsets[$charsetType];
295
             self::$defaultCharsets[$charsetType] = $charset;
296
             return $oldCharset;
297
        } else {
298
            $oldCharsets = self::$defaultCharsets;
299
            self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
300
                0,
301
                count(self::$defaultCharsets),
302
                $charset
303
            );
304
            return $oldCharsets;
305
        }
306
    }
307
308
    /**
309
     * Gets the default charset(s).
310
     *
311
     * @param int $charsetType Which charset to get. Valid values are the
312
     *     CHARSET_* constants. Any other value is treated as
313
     *     {@link self::CHARSET_ALL}.
314
     *
315
     * @return string|array The current charset. If $charsetType is
316
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
317
     *     array with the types as keys, and charsets as values.
318
     *
319
     * @see setDefaultCharset()
320
     */
321
    public static function getDefaultCharset($charsetType)
322
    {
323
        return array_key_exists($charsetType, self::$defaultCharsets)
324
            ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
325
    }
326
327
    /**
328
     * Gets the length of a seekable stream.
329
     *
330
     * Gets the length of a seekable stream.
331
     *
332
     * @param resource $stream The stream to check. The stream is assumed to be
333
     *     seekable.
334
     *
335
     * @return double The number of bytes in the stream between its current
336
     *     position and its end.
337
     */
338
    public static function seekableStreamLength($stream)
339
    {
340
        $streamPosition = (double) sprintf('%u', ftell($stream));
341
        fseek($stream, 0, SEEK_END);
342
        $streamLength = ((double) sprintf('%u', ftell($stream)))
343
            - $streamPosition;
344
        fseek($stream, $streamPosition, SEEK_SET);
345
        return $streamLength;
346
    }
347
348
    /**
349
     * Sets the charset(s) for this connection.
350
     *
351
     * Sets the charset(s) for this connection. The specified charset(s) will be
352
     * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
353
     * converted to {@link self::CHARSET_REMOTE}, and when receiving,
354
     * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
355
     * Setting  NULL to either charset will disable charset conversion, and data
356
     * will be both sent and received "as is".
357
     *
358
     * @param mixed $charset     The charset to set. If $charsetType is
359
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
360
     *     all charsets, or an array with the charset types as keys, and the
361
     *     charsets as values.
362
     * @param int   $charsetType Which charset to set. Valid values are the
363
     *     CHARSET_* constants. Any other value is treated as
364
     *     {@link self::CHARSET_ALL}.
365
     *
366
     * @return string|array The old charset. If $charsetType is
367
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
368
     *     array with the types as keys, and charsets as values.
369
     *
370
     * @see setDefaultCharset()
371
     */
372
    public function setCharset($charset, $charsetType = self::CHARSET_ALL)
373
    {
374
        if (array_key_exists($charsetType, $this->charsets)) {
375
             $oldCharset = $this->charsets[$charsetType];
376
             $this->charsets[$charsetType] = $charset;
377
             return $oldCharset;
378
        } else {
379
            $oldCharsets = $this->charsets;
380
            $this->charsets = is_array($charset) ? $charset : array_fill(
381
                0,
382
                count($this->charsets),
383
                $charset
384
            );
385
            return $oldCharsets;
386
        }
387
    }
388
389
    /**
390
     * Gets the charset(s) for this connection.
391
     *
392
     * @param int $charsetType Which charset to get. Valid values are the
393
     *     CHARSET_* constants. Any other value is treated as
394
     *     {@link self::CHARSET_ALL}.
395
     *
396
     * @return string|array The current charset. If $charsetType is
397
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
398
     *     array with the types as keys, and charsets as values.
399
     *
400
     * @see getDefaultCharset()
401
     * @see setCharset()
402
     */
403
    public function getCharset($charsetType)
404
    {
405
        return array_key_exists($charsetType, $this->charsets)
406
            ? $this->charsets[$charsetType] : $this->charsets;
407
    }
408
409
    /**
410
     * Gets the transmitter for this connection.
411
     *
412
     * @return T\TcpClient The transmitter for this connection.
413
     */
414
    public function getTransmitter()
415
    {
416
        return $this->trans;
417
    }
418
419
    /**
420
     * Sends a word.
421
     *
422
     * Sends a word and automatically encodes its length when doing so.
423
     *
424
     * @param string|resource $word     The word to send, as a string
425
     * or seekable stream.
426
     * @param string|resource $word,... Additional word fragments to be sent
427
     * as a single word.
428
     *
429
     * @return int The number of bytes sent.
430
     *
431
     * @see sendWordFromStream()
432
     * @see getNextWord()
433
     */
434
    public function sendWord($word)
435
    {
436
        $length = 0;
437
        $isCharsetConversionEnabled
438
            = null !== ($rCharset = $this->getCharset(self::CHARSET_REMOTE))
439
            && null !== ($lCharset = $this->getCharset(self::CHARSET_LOCAL));
440
        $wordFragments = array();
441
        foreach (func_get_args() as $word) {
442
            if (is_string($word)) {
443
                if ($isCharsetConversionEnabled) {
444
                    $word = iconv(
445
                        $lCharset,
0 ignored issues
show
Bug introduced by
The variable $lCharset does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
446
                        $rCharset . '//IGNORE//TRANSLIT',
447
                        $word
448
                    );
449
                }
450
                $length += strlen($word);
451
            } else {
452
                if (!self::isSeekableStream($word)) {
453
                    throw new InvalidArgumentException(
454
                        'Only seekable streams can be sent.',
455
                        InvalidArgumentException::CODE_SEEKABLE_REQUIRED
456
                    );
457
                }
458
                if ($isCharsetConversionEnabled) {
459
                    $word = self::iconvStream(
460
                        $lCharset,
0 ignored issues
show
Bug introduced by
It seems like $lCharset defined by $this->getCharset(self::CHARSET_LOCAL) on line 439 can also be of type array<string,string|null>; however, PEAR2\Net\RouterOS\Communicator::iconvStream() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
461
                        $rCharset . '//IGNORE//TRANSLIT',
462
                        $word
463
                    );
464
                }
465
                flock($word, LOCK_SH);
466
                $length += self::seekableStreamLength($word);
467
            }
468
            $wordFragments[] = $word;
469
        }
470
        static::verifyLengthSupport($length);
471
        if ($this->trans->isPersistent()) {
472
            $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
473
            $bytesSent = $this->_sendWord($length, $wordFragments);
474
            $this->trans->lock($old, true);
475
476
            return $bytesSent;
477
        }
478
479
        return $this->_sendWord($length, $wordFragments);
480
    }
481
482
    /**
483
     * Send the word fragments
484
     *
485
     * @param int|double          $length        The previously computed
486
     *     length of all fragments.
487
     * @param (string|resource)[] $wordFragments The fragments to send.
488
     *
489
     * @return int The number of bytes sent.
490
     */
491
    private function _sendWord($length, $wordFragments)
492
    {
493
        $bytesSent = $this->trans->send(self::encodeLength($length));
494
        foreach ($wordFragments as $fragment) {
495
            $bytesSent += $this->trans->send($fragment);
496
            if (!is_string($fragment)) {
497
                flock($fragment, LOCK_UN);
498
            }
499
        }
500
        return $bytesSent;
501
    }
502
503
    /**
504
     * Verifies that the length is supported.
505
     *
506
     * Verifies if the specified length is supported by the API. Throws a
507
     * {@link LengthException} if that's not the case. Currently, RouterOS
508
     * supports words up to 0xFFFFFFFF in length, so that's the only check
509
     * performed.
510
     *
511
     * @param int $length The length to verify.
512
     *
513
     * @return void
514
     */
515
    public static function verifyLengthSupport($length)
516
    {
517
        if ($length > 0xFFFFFFFF) {
518
            throw new LengthException(
519
                'Words with length above 0xFFFFFFFF are not supported.',
520
                LengthException::CODE_UNSUPPORTED,
521
                null,
522
                $length
523
            );
524
        }
525
    }
526
527
    /**
528
     * Encodes the length as required by the RouterOS API.
529
     *
530
     * @param int $length The length to encode.
531
     *
532
     * @return string The encoded length.
533
     */
534
    public static function encodeLength($length)
535
    {
536
        if ($length < 0) {
537
            throw new LengthException(
538
                'Length must not be negative.',
539
                LengthException::CODE_INVALID,
540
                null,
541
                $length
542
            );
543
        } elseif ($length < 0x80) {
544
            return chr($length);
545
        } elseif ($length < 0x4000) {
546
            return pack('n', $length |= 0x8000);
547
        } elseif ($length < 0x200000) {
548
            $length |= 0xC00000;
549
            return pack('n', $length >> 8) . chr($length & 0xFF);
550
        } elseif ($length < 0x10000000) {
551
            return pack('N', $length |= 0xE0000000);
552
        } elseif ($length <= 0xFFFFFFFF) {
553
            return chr(0xF0) . pack('N', $length);
554
        } elseif ($length <= 0x7FFFFFFFF) {
555
            $length = 'f' . base_convert($length, 10, 16);
556
            return chr(hexdec(substr($length, 0, 2))) .
557
                pack('N', hexdec(substr($length, 2)));
558
        }
559
        throw new LengthException(
560
            'Length must not be above 0x7FFFFFFFF.',
561
            LengthException::CODE_BEYOND_SHEME,
562
            null,
563
            $length
564
        );
565
    }
566
567
    /**
568
     * Get the length of the next word in queue.
569
     *
570
     * Get the length of the next word in queue without getting the word.
571
     * For pesisntent connections, note that the underlying transmitter will
572
     * be locked for receiving until either {@link self::getNextWord()} or
573
     * {@link self::getNextWordAsStream()} is called.
574
     *
575
     * @return int|double
576
     */
577
    public function getNextWordLength()
578
    {
579
        if (null === $this->nextWordLength) {
580
            if ($this->trans->isPersistent()) {
581
                $this->nextWordLock = $this->trans->lock(
582
                    T\Stream::DIRECTION_RECEIVE
583
                );
584
            }
585
            $this->nextWordLength = self::decodeLength($this->trans);
586
        }
587
        return $this->nextWordLength;
588
    }
589
590
    /**
591
     * Get the next word in queue as a string.
592
     *
593
     * Get the next word in queue as a string, after automatically decoding its
594
     * length.
595
     *
596
     * @return string The word.
597
     *
598
     * @see close()
599
     */
600
    public function getNextWord()
601
    {
602
        $word = $this->trans->receive(
603
            $this->getNextWordLength(),
604
            'word'
605
        );
606
        if (false !== $this->nextWordLock) {
607
            $this->trans->lock($this->nextWordLock, true);
608
        }
609
        $this->nextWordLength = null;
610
611
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
612
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
613
        ) {
614
            $word = iconv(
615
                $remoteCharset,
616
                $localCharset . '//IGNORE//TRANSLIT',
617
                $word
618
            );
619
        }
620
621
        return $word;
622
    }
623
624
    /**
625
     * Get the next word in queue as a stream.
626
     *
627
     * Get the next word in queue as a stream, after automatically decoding its
628
     * length.
629
     *
630
     * @return resource The word, as a stream.
631
     *
632
     * @see close()
633
     */
634
    public function getNextWordAsStream()
635
    {
636
        $filters = new T\FilterCollection();
637
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
638
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
639
        ) {
640
            $filters->append(
641
                'convert.iconv.' .
642
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
643
            );
644
        }
645
646
        $stream = $this->trans->receiveStream(
647
            $this->getNextWordLength(),
648
            $filters,
649
            'stream word'
650
        );
651
        if (false !== $this->nextWordLock) {
652
            $this->trans->lock($this->nextWordLock, true);
653
        }
654
        $this->nextWordLength = null;
655
656
        return $stream;
657
    }
658
659
    /**
660
     * Decodes the length of the incoming message.
661
     *
662
     * Decodes the length of the incoming message, as specified by the RouterOS
663
     * API.
664
     *
665
     * @param T\Stream $trans The transmitter from which to decode the length of
666
     * the incoming message.
667
     *
668
     * @return int|double The decoded length.
669
     *     Is of type "double" only for values above "2 << 31".
670
     */
671
    public static function decodeLength(T\Stream $trans)
672
    {
673
        if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
674
            $old = $trans->lock($trans::DIRECTION_RECEIVE);
675
            $length = self::_decodeLength($trans);
676
            $trans->lock($old, true);
677
            return $length;
678
        }
679
        return self::_decodeLength($trans);
680
    }
681
682
    /**
683
     * Decodes the length of the incoming message.
684
     *
685
     * Decodes the length of the incoming message, as specified by the RouterOS
686
     * API.
687
     *
688
     * Difference with the non private function is that this one doesn't perform
689
     * locking if the connection is a persistent one.
690
     *
691
     * @param T\Stream $trans The transmitter from which to decode the length of
692
     *     the incoming message.
693
     *
694
     * @return int|double The decoded length.
695
     *     Is of type "double" only for values above "2 << 31".
696
     */
697
    private static function _decodeLength(T\Stream $trans)
698
    {
699
        $byte = ord($trans->receive(1, 'initial length byte'));
700
        if ($byte & 0x80) {
701
            if (($byte & 0xC0) === 0x80) {
702
                return (($byte & 077) << 8 ) + ord($trans->receive(1));
703
            } elseif (($byte & 0xE0) === 0xC0) {
704
                $rem = unpack('n~', $trans->receive(2));
705
                return (($byte & 037) << 16 ) + $rem['~'];
706
            } elseif (($byte & 0xF0) === 0xE0) {
707
                $rem = unpack('n~/C~~', $trans->receive(3));
708
                return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
709
            } elseif (($byte & 0xF8) === 0xF0) {
710
                $rem = unpack('N~', $trans->receive(4));
711
                return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
712
                    + (double) sprintf('%u', $rem['~']);
713
            }
714
            throw new NotSupportedException(
715
                'Unknown control byte encountered.',
716
                NotSupportedException::CODE_CONTROL_BYTE,
717
                null,
718
                $byte
719
            );
720
        } else {
721
            return $byte;
722
        }
723
    }
724
725
    /**
726
     * Closes the opened connection, even if it is a persistent one.
727
     *
728
     * @return bool TRUE on success, FALSE on failure.
729
     */
730
    public function close()
731
    {
732
        return $this->trans->close();
733
    }
734
}
735