Issues (4)

src/Packer.php (2 issues)

1
<?php
2
3
/**
4
 * This file is part of the rybakit/msgpack.php package.
5
 *
6
 * (c) Eugene Leonovich <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace MessagePack;
13
14
use MessagePack\Exception\InvalidOptionException;
15
use MessagePack\Exception\PackingFailedException;
16
17
class Packer
18
{
19
    private const UTF8_REGEX = '/\A(?:
20
          [\x00-\x7F]++                      # ASCII
21
        | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
22
        |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
23
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
24
        |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
25
        |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
26
        | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
27
        |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
28
        )*+\z/x';
29
30
    /** @var bool */
31
    private $isDetectStrBin;
32
33
    /** @var bool */
34
    private $isForceStr;
35
36
    /** @var bool */
37
    private $isDetectArrMap;
38
39
    /** @var bool */
40
    private $isForceArr;
41
42
    /** @var bool */
43
    private $isForceFloat32;
44
45
    /** @var CanPack[] */
46
    private $transformers = [];
47
48
    /**
49
     * @param PackOptions|int|null $options
50
     * @param CanPack[] $transformers
51
     *
52
     * @throws InvalidOptionException
53
     */
54 212
    public function __construct($options = null, array $transformers = [])
55
    {
56 212
        if (\is_null($options)) {
57 14
            $options = PackOptions::fromDefaults();
58 204
        } elseif (!$options instanceof PackOptions) {
59 204
            $options = PackOptions::fromBitmask($options);
60
        }
61
62 212
        $this->isDetectStrBin = $options->isDetectStrBinMode();
63 212
        $this->isForceStr = $options->isForceStrMode();
64 212
        $this->isDetectArrMap = $options->isDetectArrMapMode();
65 212
        $this->isForceArr = $options->isForceArrMode();
66 212
        $this->isForceFloat32 = $options->isForceFloat32Mode();
67
68 212
        if ($transformers) {
69 209
            $this->transformers = $transformers;
70
        }
71
    }
72
73 2
    public function extendWith(CanPack $transformer, CanPack ...$transformers) : self
74
    {
75 2
        $new = clone $this;
76 2
        $new->transformers[] = $transformer;
77
78 2
        if ($transformers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $transformers of type array<integer,MessagePack\CanPack> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
79 1
            $new->transformers = \array_merge($new->transformers, $transformers);
80
        }
81
82 2
        return $new;
83
    }
84
85
    /**
86
     * @param mixed $value
87
     *
88
     * @return string
89
     */
90 137
    public function pack($value)
91
    {
92 137
        if (\is_int($value)) {
93 68
            return $this->packInt($value);
94
        }
95 97
        if (\is_string($value)) {
96 38
            if ('' === $value) {
97 1
                return $this->isForceStr || $this->isDetectStrBin ? "\xa0" : "\xc4\x00";
98
            }
99 37
            if ($this->isForceStr) {
100 8
                return $this->packStr($value);
101
            }
102 29
            if ($this->isDetectStrBin && \preg_match(self::UTF8_REGEX, $value)) {
103 20
                return $this->packStr($value);
104
            }
105
106 13
            return $this->packBin($value);
107
        }
108 72
        if (\is_array($value)) {
109 31
            if ([] === $value) {
110 1
                return $this->isDetectArrMap || $this->isForceArr ? "\x90" : "\x80";
111
            }
112 30
            if ($this->isDetectArrMap) {
113 23
                if (!isset($value[0]) && !\array_key_exists(0, $value)) {
114 14
                    return $this->packMap($value);
115
                }
116
117 13
                return \array_values($value) === $value
118 13
                    ? $this->packArray($value)
119 13
                    : $this->packMap($value);
120
            }
121
122 7
            return $this->isForceArr ? $this->packArray($value) : $this->packMap($value);
123
        }
124 48
        if (\is_null($value)) {
125 7
            return "\xc0";
126
        }
127 45
        if (\is_bool($value)) {
128 10
            return $value ? "\xc3" : "\xc2";
129
        }
130 37
        if (\is_float($value)) {
131 7
            return $this->packFloat($value);
132
        }
133 30
        if ($this->transformers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->transformers of type MessagePack\CanPack[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
134 27
            foreach ($this->transformers as $transformer) {
135 27
                if (!\is_null($packed = $transformer->pack($this, $value))) {
136 13
                    return $packed;
137
                }
138
            }
139
        }
140 17
        if ($value instanceof CanBePacked) {
141 14
            return $value->pack($this);
142
        }
143
144 3
        throw PackingFailedException::unsupportedType($value);
145
    }
146
147
    /**
148
     * @return string
149
     */
150 1
    public function packNil()
151
    {
152 1
        return "\xc0";
153
    }
154
155
    /**
156
     * @param string $bool
157
     *
158
     * @return string
159
     */
160 2
    public function packBool($bool)
161
    {
162 2
        return $bool ? "\xc3" : "\xc2";
163
    }
164
165
    /**
166
     * @param int $int
167
     *
168
     * @return string
169
     */
170 103
    public function packInt($int)
171
    {
172 103
        if ($int >= 0) {
173 71
            if ($int <= 0x7f) {
174 45
                return \chr($int);
175
            }
176 34
            if ($int <= 0xff) {
177 12
                return "\xcc".\chr($int);
178
            }
179 26
            if ($int <= 0xffff) {
180 12
                return "\xcd".\chr($int >> 8).\chr($int);
181
            }
182 16
            if ($int <= 0xffffffff) {
183 10
                return \pack('CN', 0xce, $int);
184
            }
185
186 8
            return \pack('CJ', 0xcf, $int);
187
        }
188
189 34
        if ($int >= -0x20) {
190 8
            return \chr(0xe0 | $int);
191
        }
192 26
        if ($int >= -0x80) {
193 6
            return "\xd0".\chr($int);
194
        }
195 20
        if ($int >= -0x8000) {
196 6
            return "\xd1".\chr($int >> 8).\chr($int);
197
        }
198 14
        if ($int >= -0x80000000) {
199 6
            return \pack('CN', 0xd2, $int);
200
        }
201
202 8
        return \pack('CJ', 0xd3, $int);
203
    }
204
205
    /**
206
     * @param float $float
207
     *
208
     * @return string
209
     */
210 10
    public function packFloat($float)
211
    {
212 10
        return $this->isForceFloat32
213 1
            ? "\xca".\pack('G', $float)
214 10
            : "\xcb".\pack('E', $float);
215
    }
216
217
    /**
218
     * @param float $float
219
     *
220
     * @return string
221
     */
222 2
    public function packFloat32($float)
223
    {
224 2
        return "\xca".\pack('G', $float);
225
    }
226
227
    /**
228
     * @param float $float
229
     *
230
     * @return string
231
     */
232 3
    public function packFloat64($float)
233
    {
234 3
        return "\xcb".\pack('E', $float);
235
    }
236
237
    /**
238
     * @param string $str
239
     *
240
     * @return string
241
     */
242 42
    public function packStr($str)
243
    {
244 42
        $length = \strlen($str);
245
246 42
        if ($length < 32) {
247 28
            return \chr(0xa0 | $length).$str;
248
        }
249 14
        if ($length <= 0xff) {
250 8
            return "\xd9".\chr($length).$str;
251
        }
252 6
        if ($length <= 0xffff) {
253 4
            return "\xda".\chr($length >> 8).\chr($length).$str;
254
        }
255
256 2
        return \pack('CN', 0xdb, $length).$str;
257
    }
258
259
    /**
260
     * @param string $str
261
     *
262
     * @return string
263
     */
264 21
    public function packBin($str)
265
    {
266 21
        $length = \strlen($str);
267
268 21
        if ($length <= 0xff) {
269 17
            return "\xc4".\chr($length).$str;
270
        }
271 4
        if ($length <= 0xffff) {
272 2
            return "\xc5".\chr($length >> 8).\chr($length).$str;
273
        }
274
275 2
        return \pack('CN', 0xc6, $length).$str;
276
    }
277
278
    /**
279
     * @param array $array
280
     *
281
     * @return string
282
     */
283 22
    public function packArray($array)
284
    {
285 22
        $data = $this->packArrayHeader(\count($array));
286
287 22
        foreach ($array as $val) {
288 21
            $data .= $this->pack($val);
289
        }
290
291 22
        return $data;
292
    }
293
294
    /**
295
     * @param int $size
296
     *
297
     * @return string
298
     */
299 23
    public function packArrayHeader($size)
300
    {
301 23
        if ($size <= 0xf) {
302 17
            return \chr(0x90 | $size);
303
        }
304 6
        if ($size <= 0xffff) {
305 4
            return "\xdc".\chr($size >> 8).\chr($size);
306
        }
307
308 2
        return \pack('CN', 0xdd, $size);
309
    }
310
311
    /**
312
     * @param array $map
313
     *
314
     * @return string
315
     */
316 26
    public function packMap($map)
317
    {
318 26
        $data = $this->packMapHeader(\count($map));
319
320 26
        if ($this->isForceStr) {
321 7
            foreach ($map as $key => $val) {
322 7
                $data .= \is_string($key) ? $this->packStr($key) : $this->packInt($key);
323 7
                $data .= $this->pack($val);
324
            }
325
326 7
            return $data;
327
        }
328
329 19
        if ($this->isDetectStrBin) {
330 18
            foreach ($map as $key => $val) {
331 18
                $data .= \is_string($key)
332 4
                    ? (\preg_match(self::UTF8_REGEX, $key) ? $this->packStr($key) : $this->packBin($key))
333 18
                    : $this->packInt($key);
334 18
                $data .= $this->pack($val);
335
            }
336
337 18
            return $data;
338
        }
339
340 1
        foreach ($map as $key => $val) {
341 1
            $data .= \is_string($key) ? $this->packBin($key) : $this->packInt($key);
342 1
            $data .= $this->pack($val);
343
        }
344
345 1
        return $data;
346
    }
347
348
    /**
349
     * @param int $size
350
     *
351
     * @return string
352
     */
353 27
    public function packMapHeader($size)
354
    {
355 27
        if ($size <= 0xf) {
356 21
            return \chr(0x80 | $size);
357
        }
358 6
        if ($size <= 0xffff) {
359 4
            return "\xde".\chr($size >> 8).\chr($size);
360
        }
361
362 2
        return \pack('CN', 0xdf, $size);
363
    }
364
365
    /**
366
     * @param int $type
367
     * @param string $data
368
     *
369
     * @return string
370
     */
371 30
    public function packExt($type, $data)
372
    {
373 30
        $length = \strlen($data);
374
375
        switch ($length) {
376 30
            case 1: return "\xd4".\chr($type).$data;
377 27
            case 2: return "\xd5".\chr($type).$data;
378 25
            case 4: return "\xd6".\chr($type).$data;
379 20
            case 8: return "\xd7".\chr($type).$data;
380 15
            case 16: return "\xd8".\chr($type).$data;
381
        }
382
383 13
        if ($length <= 0xff) {
384 9
            return "\xc7".\chr($length).\chr($type).$data;
385
        }
386 4
        if ($length <= 0xffff) {
387 2
            return \pack('CnC', 0xc8, $length, $type).$data;
388
        }
389
390 2
        return \pack('CNC', 0xc9, $length, $type).$data;
391
    }
392
}
393