-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #100 from mambax7/feature/ulid
- Loading branch information
Showing
2 changed files
with
795 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
<?php | ||
/* | ||
You may not change or alter any portion of this comment or credits | ||
of supporting developers from this source code or any supporting source code | ||
which is considered copyrighted (c) material of the original comment or credit authors. | ||
This program is distributed in the hope that it will be useful, | ||
but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | ||
*/ | ||
|
||
namespace Xmf; | ||
|
||
/** | ||
* Generate ULID | ||
* | ||
* @category Xmf\Ulid | ||
* @package Xmf | ||
* @author Michael Beck <[email protected]> | ||
* @copyright 2023 XOOPS Project (https://xoops.org) | ||
* @license GNU GPL 2 or later (https://www.gnu.org/licenses/gpl-2.0.html) | ||
*/ | ||
class Ulid | ||
{ | ||
const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; | ||
const ENCODING_LENGTH = 32; | ||
|
||
/** | ||
* Generate a new ULID. | ||
* | ||
* @return string The generated ULID. | ||
*/ | ||
public static function generate(bool $upperCase = true): string | ||
{ | ||
$time = self::microtimeToUlidTime(\microtime(true)); | ||
$timeChars = self::encodeTime($time); | ||
$randChars = self::encodeRandomness(); | ||
$ulid = $timeChars . $randChars; | ||
|
||
$ulid = $upperCase ? \strtoupper($ulid) : \strtolower($ulid); | ||
|
||
return $ulid; | ||
} | ||
|
||
/** | ||
* @param int $time | ||
* | ||
* @return string | ||
*/ | ||
public static function encodeTime(int $time): string | ||
{ | ||
$encodingCharsArray = str_split(self::ENCODING_CHARS); | ||
$timeChars = ''; | ||
for ($i = 0; $i < 10; $i++) { | ||
$mod = \floor($time % self::ENCODING_LENGTH); | ||
$timeChars = $encodingCharsArray[$mod] . $timeChars; | ||
$time = (int)(($time - $mod) / self::ENCODING_LENGTH); | ||
} | ||
return $timeChars; | ||
} | ||
|
||
public static function encodeRandomness(): string | ||
{ | ||
$encodingCharsArray = str_split(self::ENCODING_CHARS); | ||
$randomBytes = \random_bytes(10); // 80 bits | ||
// Check if the random bytes were generated successfully. | ||
if (false === $randomBytes) { | ||
throw new \RuntimeException('Failed to generate random bytes'); | ||
} | ||
|
||
$randChars = ''; | ||
for ($i = 0; $i < 16; $i++) { | ||
$randValue = \ord($randomBytes[$i % 10]); | ||
if (0 === $i % 2) { | ||
$randValue >>= 3; // take the upper 5 bits | ||
} else { | ||
$randValue &= 31; // take the lower 5 bits | ||
} | ||
$randChars .= $encodingCharsArray[$randValue]; | ||
} | ||
return $randChars; | ||
} | ||
|
||
/** | ||
* @param string $ulid | ||
* | ||
* @return array | ||
*/ | ||
public static function decode(string $ulid): array | ||
{ | ||
if (!self::isValid($ulid)) { | ||
throw new \InvalidArgumentException('Invalid ULID string'); | ||
} | ||
|
||
$time = self::decodeTime($ulid); | ||
$rand = self::decodeRandomness($ulid); | ||
|
||
return [ | ||
'time' => $time, | ||
'rand' => $rand, | ||
]; | ||
} | ||
|
||
/** | ||
* @param string $ulid | ||
* | ||
* @return int | ||
*/ | ||
public static function decodeTime(string $ulid): int | ||
{ | ||
// $encodingCharsArray = str_split(self::ENCODING_CHARS); | ||
|
||
// Check if the ULID string is valid. | ||
if (!self::isValid($ulid)) { | ||
throw new \InvalidArgumentException('Invalid ULID string'); | ||
} | ||
|
||
$time = 0; | ||
for ($i = 0; $i < 10; $i++) { | ||
$char = $ulid[$i]; | ||
$value = \strpos(self::ENCODING_CHARS, $char); | ||
$exponent = 9 - $i; | ||
$time += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent); | ||
} | ||
|
||
return $time; | ||
} | ||
|
||
/** | ||
* @param string $ulid | ||
* | ||
* @return int | ||
*/ | ||
public static function decodeRandomness(string $ulid): int | ||
{ | ||
if (26 !== strlen($ulid)) { | ||
throw new \InvalidArgumentException('Invalid ULID length'); // Changed line | ||
} | ||
|
||
$rand = 0; | ||
for ($i = 10; $i < 26; $i++) { | ||
$char = $ulid[$i]; | ||
$value = \strpos(self::ENCODING_CHARS, $char); | ||
|
||
// Check if the random value is within the valid range. | ||
if ($value < 0 || $value >= self::ENCODING_LENGTH) { | ||
throw new \InvalidArgumentException('Invalid ULID random value'); | ||
} | ||
$exponent = 15 - $i; | ||
$rand += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent); | ||
} | ||
|
||
return $rand; | ||
} | ||
|
||
/** | ||
* @param string $ulid | ||
* | ||
* @return bool | ||
*/ | ||
public static function isValid(string $ulid): bool | ||
{ | ||
// Check the length of the ULID string before throwing an exception. | ||
if (26 !== strlen($ulid)) { | ||
return false; | ||
} | ||
|
||
// Throw an exception if the ULID is invalid. | ||
try { | ||
self::decodeRandomness($ulid); | ||
} catch (\InvalidArgumentException $e) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @param float $microtime | ||
* | ||
* @return int | ||
*/ | ||
public static function microtimeToUlidTime(float $microtime): int | ||
{ | ||
$timestamp = $microtime * 1000000; | ||
$unixEpoch = 946684800000000; // Microseconds since the Unix epoch. | ||
|
||
return (int)($timestamp - $unixEpoch); | ||
} | ||
} | ||
|
||
|
||
|
Oops, something went wrong.