Passed
Push — master ( dcfdf8...4f44e2 )
by Richard
05:14 queued 13s
created

Ulid::isValid()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
nc 3
nop 1
dl 0
loc 15
rs 10
c 1
b 0
f 0
1
<?php
2
/*
3
 You may not change or alter any portion of this comment or credits
4
 of supporting developers from this source code or any supporting source code
5
 which is considered copyrighted (c) material of the original comment or credit authors.
6
7
 This program is distributed in the hope that it will be useful,
8
 but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
 */
11
12
namespace Xmf;
13
14
/**
15
 * Generate ULID
16
 *
17
 * @category  Xmf\Ulid
18
 * @package   Xmf
19
 * @author    Michael Beck <[email protected]>
20
 * @copyright 2023 XOOPS Project (https://xoops.org)
21
 * @license   GNU GPL 2 or later (https://www.gnu.org/licenses/gpl-2.0.html)
22
 */
23
class Ulid
24
{
25
    const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
26
    const ENCODING_LENGTH = 32;
27
28
    /**
29
     * Generate a new ULID.
30
     *
31
     * @return string The generated ULID.
32
     */
33
    public static function generate(bool $upperCase = true): string
34
    {
35
        $time = self::microtimeToUlidTime(\microtime(true));
0 ignored issues
show
Bug introduced by
It seems like microtime(true) can also be of type string; however, parameter $microtime of Xmf\Ulid::microtimeToUlidTime() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

35
        $time = self::microtimeToUlidTime(/** @scrutinizer ignore-type */ \microtime(true));
Loading history...
36
        $timeChars = self::encodeTime($time);
37
        $randChars = self::encodeRandomness();
38
        $ulid      = $timeChars . $randChars;
39
40
        $ulid = $upperCase ? \strtoupper($ulid) : \strtolower($ulid);
41
42
        return $ulid;
43
    }
44
45
    /**
46
     * @param int $time
47
     *
48
     * @return string
49
     */
50
    public static function encodeTime(int $time): string
51
    {
52
        $encodingCharsArray = str_split(self::ENCODING_CHARS);
53
        $timeChars = '';
54
        for ($i = 0; $i < 10; $i++) {
55
            $mod = \floor($time % self::ENCODING_LENGTH);
56
            $timeChars = $encodingCharsArray[$mod] . $timeChars;
57
            $time = (int)(($time - $mod) / self::ENCODING_LENGTH);
58
        }
59
        return $timeChars;
60
    }
61
62
    public static function encodeRandomness(): string
63
    {
64
        $encodingCharsArray = str_split(self::ENCODING_CHARS);
65
        $randomBytes = \random_bytes(10); // 80 bits
66
        // Check if the random bytes were generated successfully.
67
        if (false === $randomBytes) {
68
            throw new \RuntimeException('Failed to generate random bytes');
69
        }
70
71
        $randChars   = '';
72
        for ($i = 0; $i < 16; $i++) {
73
            $randValue = \ord($randomBytes[$i % 10]);
74
            if (0 === $i % 2) {
75
                $randValue >>= 3; // take the upper 5 bits
76
            } else {
77
                $randValue &= 31; // take the lower 5 bits
78
            }
79
            $randChars .= $encodingCharsArray[$randValue];
80
        }
81
        return $randChars;
82
    }
83
84
    /**
85
     * @param string $ulid
86
     *
87
     * @return array
88
     */
89
    public static function decode(string $ulid): array
90
    {
91
        if (!self::isValid($ulid)) {
92
            throw new \InvalidArgumentException('Invalid ULID string');
93
        }
94
95
        $time = self::decodeTime($ulid);
96
        $rand = self::decodeRandomness($ulid);
97
98
        return [
99
            'time' => $time,
100
            'rand' => $rand,
101
        ];
102
    }
103
104
    /**
105
     * @param string $ulid
106
     *
107
     * @return int
108
     */
109
    public static function decodeTime(string $ulid): int
110
    {
111
//        $encodingCharsArray = str_split(self::ENCODING_CHARS);
112
113
        // Check if the ULID string is valid.
114
        if (!self::isValid($ulid)) {
115
            throw new \InvalidArgumentException('Invalid ULID string');
116
        }
117
118
        $time = 0;
119
        for ($i = 0; $i < 10; $i++) {
120
            $char = $ulid[$i];
121
            $value = \strpos(self::ENCODING_CHARS, $char);
122
            $exponent = 9 - $i;
123
            $time += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
124
        }
125
126
        return $time;
127
    }
128
129
    /**
130
     * @param string $ulid
131
     *
132
     * @return int
133
     */
134
    public static function decodeRandomness(string $ulid): int
135
    {
136
        if (26 !== strlen($ulid)) {
137
            throw new \InvalidArgumentException('Invalid ULID length');  // Changed line
138
        }
139
140
        $rand = 0;
141
        for ($i = 10; $i < 26; $i++) {
142
            $char = $ulid[$i];
143
            $value = \strpos(self::ENCODING_CHARS, $char);
144
145
            // Check if the random value is within the valid range.
146
            if ($value < 0 || $value >= self::ENCODING_LENGTH) {
147
                throw new \InvalidArgumentException('Invalid ULID random value');
148
            }
149
            $exponent = 15 - $i;
150
            $rand += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
151
        }
152
153
        return $rand;
154
    }
155
156
    /**
157
     * @param string $ulid
158
     *
159
     * @return bool
160
     */
161
    public static function isValid(string $ulid): bool
162
    {
163
        // Check the length of the ULID string before throwing an exception.
164
        if (26 !== strlen($ulid)) {
165
            return false;
166
        }
167
168
        // Throw an exception if the ULID is invalid.
169
        try {
170
            self::decodeRandomness($ulid);
171
        } catch (\InvalidArgumentException $e) {
172
                return false;
173
}
174
175
        return true;
176
    }
177
178
    /**
179
     * @param float $microtime
180
     *
181
     * @return int
182
     */
183
    public static function microtimeToUlidTime(float $microtime): int
184
    {
185
        $timestamp = $microtime * 1000000;
186
        $unixEpoch = 946684800000000; // Microseconds since the Unix epoch.
187
188
        return (int)($timestamp - $unixEpoch);
189
    }
190
}
191
192
193
194