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 <mambax7@gmail.com>
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));
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: