Completed
Branch develop (b42f4c)
by Vasil
13:57
created

Communicator::getNextWordLength()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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