Completed
Push — develop ( 7482a6...c28e37 )
by Vasil
03:32
created

Communicator   C

Complexity

Total Complexity 63

Size/Duplication

Total Lines 659
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 30
Bugs 18 Features 10
Metric Value
wmc 63
c 30
b 18
f 10
lcom 2
cbo 7
dl 0
loc 659
rs 5.4073

19 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 57 8
A __invoke() 0 5 2
A isSeekableStream() 0 8 2
B iconvStream() 0 24 3
A setDefaultCharset() 0 18 3
A getDefaultCharset() 0 5 2
A seekableStreamLength() 0 9 1
A setCharset() 0 16 3
A getCharset() 0 5 2
A getTransmitter() 0 4 1
A sendWord() 0 21 4
B sendWordFromStream() 0 33 4
A verifyLengthSupport() 0 11 2
C encodeLength() 0 32 8
B getNextWord() 0 28 4
B getNextWordAsStream() 0 30 4
A decodeLength() 0 10 3
B _decodeLength() 0 27 6
A close() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Communicator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Communicator, and based on these observations, apply Extract Interface, too.

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
     * The transmitter for the connection.
92
     *
93
     * @var T\TcpClient
94
     */
95
    protected $trans;
96
97
    /**
98
     * Creates a new connection with the specified options.
99
     *
100
     * @param string        $host    Hostname (IP or domain) of RouterOS.
101
     * @param int|null      $port    The port on which the RouterOS host
102
     *     provides the API service. You can also specify NULL, in which case
103
     *     the port will automatically be chosen between 8728 and 8729,
104
     *     depending on the value of $crypto.
105
     * @param bool          $persist Whether or not the connection should be a
106
     *     persistent one.
107
     * @param double|null   $timeout The timeout for the connection.
108
     * @param string        $key     A string that uniquely identifies the
109
     *     connection.
110
     * @param string        $crypto  The encryption for this connection.
111
     *     Must be one of the PEAR2\Net\Transmitter\NetworkStream::CRYPTO_*
112
     *     constants. Off by default. RouterOS currently supports only TLS, but
113
     *     the setting is provided in this fashion for forward compatibility's
114
     *     sake. And for the sake of simplicity, if you specify an encryption,
115
     *     don't specify a context and your default context uses the value
116
     *     "DEFAULT" for ciphers, "ADH" will be automatically added to the list
117
     *     of ciphers.
118
     * @param resource|null $context A context for the socket.
119
     *
120
     * @see sendWord()
121
     */
122
    public function __construct(
123
        $host,
124
        $port = 8728,
125
        $persist = false,
126
        $timeout = null,
127
        $key = '',
128
        $crypto = T\NetworkStream::CRYPTO_OFF,
129
        $context = null
130
    ) {
131
        $isUnencrypted = T\NetworkStream::CRYPTO_OFF === $crypto;
132
        if (($context === null) && !$isUnencrypted) {
133
            $context = stream_context_get_default();
134
            $opts = stream_context_get_options($context);
135
            if (!isset($opts['ssl']['ciphers'])
136
                || 'DEFAULT' === $opts['ssl']['ciphers']
137
            ) {
138
                stream_context_set_option(
139
                    $context,
140
                    array(
141
                        'ssl' => array(
142
                            'ciphers' => 'ADH',
143
                            'verify_peer' => false,
144
                            'verify_peer_name' => false
145
                        )
146
                    )
147
                );
148
            }
149
        }
150
        // @codeCoverageIgnoreStart
151
        // The $port is customizable in testing.
152
        if (null === $port) {
153
            $port = $isUnencrypted ? 8728 : 8729;
154
        }
155
        // @codeCoverageIgnoreEnd
156
157
        try {
158
            $this->trans = new T\TcpClient(
159
                $host,
160
                $port,
161
                $persist,
162
                $timeout,
163
                $key,
164
                $crypto,
165
                $context
166
            );
167
        } catch (T\Exception $e) {
168
            throw new SocketException(
169
                'Error connecting to RouterOS',
170
                SocketException::CODE_CONNECTION_FAIL,
171
                $e
172
            );
173
        }
174
        $this->setCharset(
175
            self::getDefaultCharset(self::CHARSET_ALL),
176
            self::CHARSET_ALL
177
        );
178
    }
179
180
    /**
181
     * A shorthand gateway.
182
     *
183
     * This is a magic PHP method that allows you to call the object as a
184
     * function. Depending on the argument given, one of the other functions in
185
     * the class is invoked and its returned value is returned by this function.
186
     *
187
     * @param string|null $string A string of the word to send, or NULL to get
188
     *     the next word as a string.
189
     *
190
     * @return int|string If a string is provided, returns the number of bytes
191
     *     sent, otherwise returns the next word as a string.
192
     */
193
    public function __invoke($string = null)
194
    {
195
        return null === $string ? $this->getNextWord()
196
            : $this->sendWord($string);
197
    }
198
199
    /**
200
     * Checks whether a variable is a seekable stream resource.
201
     *
202
     * @param mixed $var The value to check.
203
     *
204
     * @return bool TRUE if $var is a seekable stream, FALSE otherwise.
205
     */
206
    public static function isSeekableStream($var)
207
    {
208
        if (T\Stream::isStream($var)) {
209
            $meta = stream_get_meta_data($var);
210
            return $meta['seekable'];
211
        }
212
        return false;
213
    }
214
215
    /**
216
     * Uses iconv to convert a stream from one charset to another.
217
     *
218
     * @param string   $inCharset  The charset of the stream.
219
     * @param string   $outCharset The desired resulting charset.
220
     * @param resource $stream     The stream to convert. The stream is assumed
221
     *     to be seekable, and is read from its current position to its end,
222
     *     after which, it is seeked back to its starting position.
223
     *
224
     * @return resource A new stream that uses the $out_charset. The stream is a
225
     *     subset from the original stream, from its current position to its
226
     *     end, seeked at its start.
227
     */
228
    public static function iconvStream($inCharset, $outCharset, $stream)
229
    {
230
        $bytes = 0;
231
        $result = fopen('php://temp', 'r+b');
232
        $iconvFilter = stream_filter_append(
233
            $result,
234
            'convert.iconv.' . $inCharset . '.' . $outCharset,
235
            STREAM_FILTER_WRITE
236
        );
237
238
        flock($stream, LOCK_SH);
239
        $reader = new T\Stream($stream, false);
240
        $writer = new T\Stream($result, false);
241
        $chunkSize = $reader->getChunk(T\Stream::DIRECTION_RECEIVE);
242
        while ($reader->isAvailable() && $reader->isDataAwaiting()) {
243
            $bytes += $writer->send(fread($stream, $chunkSize));
244
        }
245
        fseek($stream, -$bytes, SEEK_CUR);
246
        flock($stream, LOCK_UN);
247
248
        stream_filter_remove($iconvFilter);
249
        rewind($result);
250
        return $result;
251
    }
252
253
    /**
254
     * Sets the default charset(s) for new connections.
255
     *
256
     * @param mixed $charset     The charset to set. If $charsetType is
257
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
258
     *     all charsets, or an array with the charset types as keys, and the
259
     *     charsets as values.
260
     * @param int   $charsetType Which charset to set. Valid values are the
261
     *     CHARSET_* constants. Any other value is treated as
262
     *     {@link self::CHARSET_ALL}.
263
     *
264
     * @return string|array The old charset. If $charsetType is
265
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
266
     *     array with the types as keys, and charsets as values.
267
     *
268
     * @see setCharset()
269
     */
270
    public static function setDefaultCharset(
271
        $charset,
272
        $charsetType = self::CHARSET_ALL
273
    ) {
274
        if (array_key_exists($charsetType, self::$defaultCharsets)) {
275
             $oldCharset = self::$defaultCharsets[$charsetType];
276
             self::$defaultCharsets[$charsetType] = $charset;
277
             return $oldCharset;
278
        } else {
279
            $oldCharsets = self::$defaultCharsets;
280
            self::$defaultCharsets = is_array($charset) ? $charset : array_fill(
281
                0,
282
                count(self::$defaultCharsets),
283
                $charset
284
            );
285
            return $oldCharsets;
286
        }
287
    }
288
289
    /**
290
     * Gets the default charset(s).
291
     *
292
     * @param int $charsetType Which charset to get. Valid values are the
293
     *     CHARSET_* constants. Any other value is treated as
294
     *     {@link self::CHARSET_ALL}.
295
     *
296
     * @return string|array The current charset. If $charsetType is
297
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
298
     *     array with the types as keys, and charsets as values.
299
     *
300
     * @see setDefaultCharset()
301
     */
302
    public static function getDefaultCharset($charsetType)
303
    {
304
        return array_key_exists($charsetType, self::$defaultCharsets)
305
            ? self::$defaultCharsets[$charsetType] : self::$defaultCharsets;
306
    }
307
308
    /**
309
     * Gets the length of a seekable stream.
310
     *
311
     * Gets the length of a seekable stream.
312
     *
313
     * @param resource $stream The stream to check. The stream is assumed to be
314
     *     seekable.
315
     *
316
     * @return double The number of bytes in the stream between its current
317
     *     position and its end.
318
     */
319
    public static function seekableStreamLength($stream)
320
    {
321
        $streamPosition = (double) sprintf('%u', ftell($stream));
322
        fseek($stream, 0, SEEK_END);
323
        $streamLength = ((double) sprintf('%u', ftell($stream)))
324
            - $streamPosition;
325
        fseek($stream, $streamPosition, SEEK_SET);
326
        return $streamLength;
327
    }
328
329
    /**
330
     * Sets the charset(s) for this connection.
331
     *
332
     * Sets the charset(s) for this connection. The specified charset(s) will be
333
     * used for all future words. When sending, {@link self::CHARSET_LOCAL} is
334
     * converted to {@link self::CHARSET_REMOTE}, and when receiving,
335
     * {@link self::CHARSET_REMOTE} is converted to {@link self::CHARSET_LOCAL}.
336
     * Setting  NULL to either charset will disable charset conversion, and data
337
     * will be both sent and received "as is".
338
     *
339
     * @param mixed $charset     The charset to set. If $charsetType is
340
     *     {@link self::CHARSET_ALL}, you can supply either a string to use for
341
     *     all charsets, or an array with the charset types as keys, and the
342
     *     charsets as values.
343
     * @param int   $charsetType Which charset to set. Valid values are the
344
     *     CHARSET_* constants. Any other value is treated as
345
     *     {@link self::CHARSET_ALL}.
346
     *
347
     * @return string|array The old charset. If $charsetType is
348
     *     {@link self::CHARSET_ALL}, the old values will be returned as an
349
     *     array with the types as keys, and charsets as values.
350
     *
351
     * @see setDefaultCharset()
352
     */
353
    public function setCharset($charset, $charsetType = self::CHARSET_ALL)
354
    {
355
        if (array_key_exists($charsetType, $this->charsets)) {
356
             $oldCharset = $this->charsets[$charsetType];
357
             $this->charsets[$charsetType] = $charset;
358
             return $oldCharset;
359
        } else {
360
            $oldCharsets = $this->charsets;
361
            $this->charsets = is_array($charset) ? $charset : array_fill(
362
                0,
363
                count($this->charsets),
364
                $charset
365
            );
366
            return $oldCharsets;
367
        }
368
    }
369
370
    /**
371
     * Gets the charset(s) for this connection.
372
     *
373
     * @param int $charsetType Which charset to get. Valid values are the
374
     *     CHARSET_* constants. Any other value is treated as
375
     *     {@link self::CHARSET_ALL}.
376
     *
377
     * @return string|array The current charset. If $charsetType is
378
     *     {@link self::CHARSET_ALL}, the current values will be returned as an
379
     *     array with the types as keys, and charsets as values.
380
     *
381
     * @see getDefaultCharset()
382
     * @see setCharset()
383
     */
384
    public function getCharset($charsetType)
385
    {
386
        return array_key_exists($charsetType, $this->charsets)
387
            ? $this->charsets[$charsetType] : $this->charsets;
388
    }
389
390
    /**
391
     * Gets the transmitter for this connection.
392
     *
393
     * @return T\TcpClient The transmitter for this connection.
394
     */
395
    public function getTransmitter()
396
    {
397
        return $this->trans;
398
    }
399
400
    /**
401
     * Sends a word.
402
     *
403
     * Sends a word and automatically encodes its length when doing so.
404
     *
405
     * @param string $word The word to send.
406
     *
407
     * @return int The number of bytes sent.
408
     *
409
     * @see sendWordFromStream()
410
     * @see getNextWord()
411
     */
412
    public function sendWord($word)
413
    {
414
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
415
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
416
        ) {
417
            $word = iconv(
418
                $localCharset,
419
                $remoteCharset . '//IGNORE//TRANSLIT',
420
                $word
421
            );
422
        }
423
        $length = strlen($word);
424
        static::verifyLengthSupport($length);
425
        if ($this->trans->isPersistent()) {
426
            $old = $this->trans->lock(T\Stream::DIRECTION_SEND);
427
            $bytes = $this->trans->send(self::encodeLength($length) . $word);
428
            $this->trans->lock($old, true);
429
            return $bytes;
430
        }
431
        return $this->trans->send(self::encodeLength($length) . $word);
432
    }
433
434
    /**
435
     * Sends a word based on a stream.
436
     *
437
     * Sends a word based on a stream and automatically encodes its length when
438
     * doing so. The stream is read from its current position to its end, and
439
     * then returned to its current position. Because of those operations, the
440
     * supplied stream must be seekable.
441
     *
442
     * @param string   $prefix A string to prepend before the stream contents.
443
     * @param resource $stream The seekable stream to send.
444
     *
445
     * @return int The number of bytes sent.
446
     *
447
     * @see sendWord()
448
     */
449
    public function sendWordFromStream($prefix, $stream)
450
    {
451
        if (!self::isSeekableStream($stream)) {
452
            throw new InvalidArgumentException(
453
                'The stream must be seekable.',
454
                InvalidArgumentException::CODE_SEEKABLE_REQUIRED
455
            );
456
        }
457
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
458
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
459
        ) {
460
            $prefix = iconv(
461
                $localCharset,
462
                $remoteCharset . '//IGNORE//TRANSLIT',
463
                $prefix
464
            );
465
            $stream = self::iconvStream(
466
                $localCharset,
467
                $remoteCharset . '//IGNORE//TRANSLIT',
468
                $stream
469
            );
470
        }
471
472
        flock($stream, LOCK_SH);
473
        $totalLength = strlen($prefix) + self::seekableStreamLength($stream);
474
        static::verifyLengthSupport($totalLength);
475
476
        $bytes = $this->trans->send(self::encodeLength($totalLength) . $prefix);
477
        $bytes += $this->trans->send($stream);
478
479
        flock($stream, LOCK_UN);
480
        return $bytes;
481
    }
482
483
    /**
484
     * Verifies that the length is supported.
485
     *
486
     * Verifies if the specified length is supported by the API. Throws a
487
     * {@link LengthException} if that's not the case. Currently, RouterOS
488
     * supports words up to 0xFFFFFFFF in length, so that's the only check
489
     * performed.
490
     *
491
     * @param int $length The length to verify.
492
     *
493
     * @return void
494
     */
495
    public static function verifyLengthSupport($length)
496
    {
497
        if ($length > 0xFFFFFFFF) {
498
            throw new LengthException(
499
                'Words with length above 0xFFFFFFFF are not supported.',
500
                LengthException::CODE_UNSUPPORTED,
501
                null,
502
                $length
503
            );
504
        }
505
    }
506
507
    /**
508
     * Encodes the length as required by the RouterOS API.
509
     *
510
     * @param int $length The length to encode.
511
     *
512
     * @return string The encoded length.
513
     */
514
    public static function encodeLength($length)
515
    {
516
        if ($length < 0) {
517
            throw new LengthException(
518
                'Length must not be negative.',
519
                LengthException::CODE_INVALID,
520
                null,
521
                $length
522
            );
523
        } elseif ($length < 0x80) {
524
            return chr($length);
525
        } elseif ($length < 0x4000) {
526
            return pack('n', $length |= 0x8000);
527
        } elseif ($length < 0x200000) {
528
            $length |= 0xC00000;
529
            return pack('n', $length >> 8) . chr($length & 0xFF);
530
        } elseif ($length < 0x10000000) {
531
            return pack('N', $length |= 0xE0000000);
532
        } elseif ($length <= 0xFFFFFFFF) {
533
            return chr(0xF0) . pack('N', $length);
534
        } elseif ($length <= 0x7FFFFFFFF) {
535
            $length = 'f' . base_convert($length, 10, 16);
536
            return chr(hexdec(substr($length, 0, 2))) .
537
                pack('N', hexdec(substr($length, 2)));
538
        }
539
        throw new LengthException(
540
            'Length must not be above 0x7FFFFFFFF.',
541
            LengthException::CODE_BEYOND_SHEME,
542
            null,
543
            $length
544
        );
545
    }
546
547
    /**
548
     * Get the next word in queue as a string.
549
     *
550
     * Get the next word in queue as a string, after automatically decoding its
551
     * length.
552
     *
553
     * @return string The word.
554
     *
555
     * @see close()
556
     */
557
    public function getNextWord()
558
    {
559
        if ($this->trans->isPersistent()) {
560
            $old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
561
            $word = $this->trans->receive(
562
                self::decodeLength($this->trans),
563
                'word'
564
            );
565
            $this->trans->lock($old, true);
1 ignored issue
show
Security Bug introduced by
It seems like $old defined by $this->trans->lock(\PEAR...eam::DIRECTION_RECEIVE) on line 560 can also be of type false; however, PEAR2\Net\Transmitter\TcpClient::lock() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
566
        } else {
567
            $word = $this->trans->receive(
568
                self::decodeLength($this->trans),
569
                'word'
570
            );
571
        }
572
573
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
574
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
575
        ) {
576
            $word = iconv(
577
                $remoteCharset,
578
                $localCharset . '//IGNORE//TRANSLIT',
579
                $word
580
            );
581
        }
582
583
        return $word;
584
    }
585
586
    /**
587
     * Get the next word in queue as a stream.
588
     *
589
     * Get the next word in queue as a stream, after automatically decoding its
590
     * length.
591
     *
592
     * @return resource The word, as a stream.
593
     *
594
     * @see close()
595
     */
596
    public function getNextWordAsStream()
597
    {
598
        $filters = new T\FilterCollection();
599
        if (null !== ($remoteCharset = $this->getCharset(self::CHARSET_REMOTE))
600
            && null !== ($localCharset = $this->getCharset(self::CHARSET_LOCAL))
601
        ) {
602
            $filters->append(
603
                'convert.iconv.' .
604
                $remoteCharset . '.' . $localCharset . '//IGNORE//TRANSLIT'
605
            );
606
        }
607
608
        if ($this->trans->isPersistent()) {
609
            $old = $this->trans->lock(T\Stream::DIRECTION_RECEIVE);
610
            $stream = $this->trans->receiveStream(
611
                self::decodeLength($this->trans),
612
                $filters,
613
                'stream word'
614
            );
615
            $this->trans->lock($old, true);
1 ignored issue
show
Security Bug introduced by
It seems like $old defined by $this->trans->lock(\PEAR...eam::DIRECTION_RECEIVE) on line 609 can also be of type false; however, PEAR2\Net\Transmitter\TcpClient::lock() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
616
        } else {
617
            $stream = $this->trans->receiveStream(
618
                self::decodeLength($this->trans),
619
                $filters,
620
                'stream word'
621
            );
622
        }
623
624
        return $stream;
625
    }
626
627
    /**
628
     * Decodes the length of the incoming message.
629
     *
630
     * Decodes the length of the incoming message, as specified by the RouterOS
631
     * API.
632
     *
633
     * @param T\Stream $trans The transmitter from which to decode the length of
634
     * the incoming message.
635
     *
636
     * @return int|double The decoded length.
637
     *     Is of type "double" only for values above "2 << 31".
638
     */
639
    public static function decodeLength(T\Stream $trans)
640
    {
641
        if ($trans->isPersistent() && $trans instanceof T\TcpClient) {
642
            $old = $trans->lock($trans::DIRECTION_RECEIVE);
643
            $length = self::_decodeLength($trans);
644
            $trans->lock($old, true);
645
            return $length;
646
        }
647
        return self::_decodeLength($trans);
648
    }
649
650
    /**
651
     * Decodes the length of the incoming message.
652
     *
653
     * Decodes the length of the incoming message, as specified by the RouterOS
654
     * API.
655
     *
656
     * Difference with the non private function is that this one doesn't perform
657
     * locking if the connection is a persistent one.
658
     *
659
     * @param T\Stream $trans The transmitter from which to decode the length of
660
     *     the incoming message.
661
     *
662
     * @return int|double The decoded length.
663
     *     Is of type "double" only for values above "2 << 31".
664
     */
665
    private static function _decodeLength(T\Stream $trans)
666
    {
667
        $byte = ord($trans->receive(1, 'initial length byte'));
668
        if ($byte & 0x80) {
669
            if (($byte & 0xC0) === 0x80) {
670
                return (($byte & 077) << 8 ) + ord($trans->receive(1));
671
            } elseif (($byte & 0xE0) === 0xC0) {
672
                $rem = unpack('n~', $trans->receive(2));
673
                return (($byte & 037) << 16 ) + $rem['~'];
674
            } elseif (($byte & 0xF0) === 0xE0) {
675
                $rem = unpack('n~/C~~', $trans->receive(3));
676
                return (($byte & 017) << 24 ) + ($rem['~'] << 8) + $rem['~~'];
677
            } elseif (($byte & 0xF8) === 0xF0) {
678
                $rem = unpack('N~', $trans->receive(4));
679
                return (($byte & 007) * 0x100000000/* '<< 32' or '2^32' */)
680
                    + (double) sprintf('%u', $rem['~']);
681
            }
682
            throw new NotSupportedException(
683
                'Unknown control byte encountered.',
684
                NotSupportedException::CODE_CONTROL_BYTE,
685
                null,
686
                $byte
687
            );
688
        } else {
689
            return $byte;
690
        }
691
    }
692
693
    /**
694
     * Closes the opened connection, even if it is a persistent one.
695
     *
696
     * @return bool TRUE on success, FALSE on failure.
697
     */
698
    public function close()
699
    {
700
        return $this->trans->close();
701
    }
702
}
703