1<?php
2
3/**
4 * This file is part of the ramsey/uuid library
5 *
6 * For the full copyright and license information, please view the LICENSE
7 * file that was distributed with this source code.
8 *
9 * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
10 * @license http://opensource.org/licenses/MIT MIT
11 */
12
13declare(strict_types=1);
14
15namespace Ramsey\Uuid\Guid;
16
17use Ramsey\Uuid\Exception\InvalidArgumentException;
18use Ramsey\Uuid\Fields\SerializableFieldsTrait;
19use Ramsey\Uuid\Rfc4122\FieldsInterface;
20use Ramsey\Uuid\Rfc4122\NilTrait;
21use Ramsey\Uuid\Rfc4122\VariantTrait;
22use Ramsey\Uuid\Rfc4122\VersionTrait;
23use Ramsey\Uuid\Type\Hexadecimal;
24use Ramsey\Uuid\Uuid;
25
26use function bin2hex;
27use function dechex;
28use function hexdec;
29use function pack;
30use function sprintf;
31use function str_pad;
32use function strlen;
33use function substr;
34use function unpack;
35
36use const STR_PAD_LEFT;
37
38/**
39 * GUIDs are comprised of a set of named fields, according to RFC 4122
40 *
41 * @see Guid
42 *
43 * @psalm-immutable
44 */
45final class Fields implements FieldsInterface
46{
47    use NilTrait;
48    use SerializableFieldsTrait;
49    use VariantTrait;
50    use VersionTrait;
51
52    /**
53     * @var string
54     */
55    private $bytes;
56
57    /**
58     * @param string $bytes A 16-byte binary string representation of a UUID
59     *
60     * @throws InvalidArgumentException if the byte string is not exactly 16 bytes
61     * @throws InvalidArgumentException if the byte string does not represent a GUID
62     * @throws InvalidArgumentException if the byte string does not contain a valid version
63     */
64    public function __construct(string $bytes)
65    {
66        if (strlen($bytes) !== 16) {
67            throw new InvalidArgumentException(
68                'The byte string must be 16 bytes long; '
69                . 'received ' . strlen($bytes) . ' bytes'
70            );
71        }
72
73        $this->bytes = $bytes;
74
75        if (!$this->isCorrectVariant()) {
76            throw new InvalidArgumentException(
77                'The byte string received does not conform to the RFC '
78                . '4122 or Microsoft Corporation variants'
79            );
80        }
81
82        if (!$this->isCorrectVersion()) {
83            throw new InvalidArgumentException(
84                'The byte string received does not contain a valid version'
85            );
86        }
87    }
88
89    public function getBytes(): string
90    {
91        return $this->bytes;
92    }
93
94    public function getTimeLow(): Hexadecimal
95    {
96        // Swap the bytes from little endian to network byte order.
97        $hex = unpack(
98            'H*',
99            pack(
100                'v*',
101                hexdec(bin2hex(substr($this->bytes, 2, 2))),
102                hexdec(bin2hex(substr($this->bytes, 0, 2)))
103            )
104        );
105
106        return new Hexadecimal((string) ($hex[1] ?? ''));
107    }
108
109    public function getTimeMid(): Hexadecimal
110    {
111        // Swap the bytes from little endian to network byte order.
112        $hex = unpack(
113            'H*',
114            pack(
115                'v',
116                hexdec(bin2hex(substr($this->bytes, 4, 2)))
117            )
118        );
119
120        return new Hexadecimal((string) ($hex[1] ?? ''));
121    }
122
123    public function getTimeHiAndVersion(): Hexadecimal
124    {
125        // Swap the bytes from little endian to network byte order.
126        $hex = unpack(
127            'H*',
128            pack(
129                'v',
130                hexdec(bin2hex(substr($this->bytes, 6, 2)))
131            )
132        );
133
134        return new Hexadecimal((string) ($hex[1] ?? ''));
135    }
136
137    public function getTimestamp(): Hexadecimal
138    {
139        return new Hexadecimal(sprintf(
140            '%03x%04s%08s',
141            hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff,
142            $this->getTimeMid()->toString(),
143            $this->getTimeLow()->toString()
144        ));
145    }
146
147    public function getClockSeq(): Hexadecimal
148    {
149        $clockSeq = hexdec(bin2hex(substr($this->bytes, 8, 2))) & 0x3fff;
150
151        return new Hexadecimal(str_pad(dechex($clockSeq), 4, '0', STR_PAD_LEFT));
152    }
153
154    public function getClockSeqHiAndReserved(): Hexadecimal
155    {
156        return new Hexadecimal(bin2hex(substr($this->bytes, 8, 1)));
157    }
158
159    public function getClockSeqLow(): Hexadecimal
160    {
161        return new Hexadecimal(bin2hex(substr($this->bytes, 9, 1)));
162    }
163
164    public function getNode(): Hexadecimal
165    {
166        return new Hexadecimal(bin2hex(substr($this->bytes, 10)));
167    }
168
169    public function getVersion(): ?int
170    {
171        if ($this->isNil()) {
172            return null;
173        }
174
175        $parts = unpack('n*', $this->bytes);
176
177        return ((int) $parts[4] >> 4) & 0x00f;
178    }
179
180    private function isCorrectVariant(): bool
181    {
182        if ($this->isNil()) {
183            return true;
184        }
185
186        $variant = $this->getVariant();
187
188        return $variant === Uuid::RFC_4122 || $variant === Uuid::RESERVED_MICROSOFT;
189    }
190}
191