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\Generator;
16
17use Ramsey\Uuid\Converter\TimeConverterInterface;
18use Ramsey\Uuid\Exception\InvalidArgumentException;
19use Ramsey\Uuid\Exception\RandomSourceException;
20use Ramsey\Uuid\Exception\TimeSourceException;
21use Ramsey\Uuid\Provider\NodeProviderInterface;
22use Ramsey\Uuid\Provider\TimeProviderInterface;
23use Ramsey\Uuid\Type\Hexadecimal;
24use Throwable;
25
26use function ctype_xdigit;
27use function dechex;
28use function hex2bin;
29use function is_int;
30use function pack;
31use function sprintf;
32use function str_pad;
33use function strlen;
34
35use const STR_PAD_LEFT;
36
37/**
38 * DefaultTimeGenerator generates strings of binary data based on a node ID,
39 * clock sequence, and the current time
40 */
41class DefaultTimeGenerator implements TimeGeneratorInterface
42{
43    /**
44     * @var NodeProviderInterface
45     */
46    private $nodeProvider;
47
48    /**
49     * @var TimeConverterInterface
50     */
51    private $timeConverter;
52
53    /**
54     * @var TimeProviderInterface
55     */
56    private $timeProvider;
57
58    public function __construct(
59        NodeProviderInterface $nodeProvider,
60        TimeConverterInterface $timeConverter,
61        TimeProviderInterface $timeProvider
62    ) {
63        $this->nodeProvider = $nodeProvider;
64        $this->timeConverter = $timeConverter;
65        $this->timeProvider = $timeProvider;
66    }
67
68    /**
69     * @throws InvalidArgumentException if the parameters contain invalid values
70     * @throws RandomSourceException if random_int() throws an exception/error
71     *
72     * @inheritDoc
73     */
74    public function generate($node = null, ?int $clockSeq = null): string
75    {
76        if ($node instanceof Hexadecimal) {
77            $node = $node->toString();
78        }
79
80        $node = $this->getValidNode($node);
81
82        if ($clockSeq === null) {
83            try {
84                // This does not use "stable storage"; see RFC 4122, Section 4.2.1.1.
85                $clockSeq = random_int(0, 0x3fff);
86            } catch (Throwable $exception) {
87                throw new RandomSourceException(
88                    $exception->getMessage(),
89                    (int) $exception->getCode(),
90                    $exception
91                );
92            }
93        }
94
95        $time = $this->timeProvider->getTime();
96
97        $uuidTime = $this->timeConverter->calculateTime(
98            $time->getSeconds()->toString(),
99            $time->getMicroseconds()->toString()
100        );
101
102        $timeHex = str_pad($uuidTime->toString(), 16, '0', STR_PAD_LEFT);
103
104        if (strlen($timeHex) !== 16) {
105            throw new TimeSourceException(sprintf(
106                'The generated time of \'%s\' is larger than expected',
107                $timeHex
108            ));
109        }
110
111        $timeBytes = (string) hex2bin($timeHex);
112
113        return $timeBytes[4] . $timeBytes[5] . $timeBytes[6] . $timeBytes[7]
114            . $timeBytes[2] . $timeBytes[3]
115            . $timeBytes[0] . $timeBytes[1]
116            . pack('n*', $clockSeq)
117            . $node;
118    }
119
120    /**
121     * Uses the node provider given when constructing this instance to get
122     * the node ID (usually a MAC address)
123     *
124     * @param string|int|null $node A node value that may be used to override the node provider
125     *
126     * @return string 6-byte binary string representation of the node
127     *
128     * @throws InvalidArgumentException
129     */
130    private function getValidNode($node): string
131    {
132        if ($node === null) {
133            $node = $this->nodeProvider->getNode();
134        }
135
136        // Convert the node to hex, if it is still an integer.
137        if (is_int($node)) {
138            $node = dechex($node);
139        }
140
141        if (!ctype_xdigit((string) $node) || strlen((string) $node) > 12) {
142            throw new InvalidArgumentException('Invalid node value');
143        }
144
145        return (string) hex2bin(str_pad((string) $node, 12, '0', STR_PAD_LEFT));
146    }
147}
148