File Coverage

blib/lib/Data/ULID.pm
Criterion Covered Total %
statement 87 98 88.7
branch 15 26 57.6
condition 4 8 50.0
subroutine 25 27 92.5
pod 0 5 0.0
total 131 164 79.8


line stmt bran cond sub pod time code
1             package Data::ULID;
2              
3 2     2   68583 use strict;
  2         21  
  2         64  
4 2     2   10 use warnings;
  2         5  
  2         83  
5              
6             our $VERSION = '1.1.3_03';
7              
8 2     2   11 use base qw(Exporter);
  2         13  
  2         376  
9             our @EXPORT_OK = qw/ulid binary_ulid ulid_date ulid_to_uuid uuid_to_ulid/;
10             our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
11              
12 2     2   1082 use Time::HiRes qw/time/;
  2         2890  
  2         8  
13 2     2   1407 use Crypt::PRNG qw/random_bytes/;
  2         7008  
  2         148  
14              
15 2     2   18 use constant HAS_DATETIME => eval { require DateTime; 1 };
  2         4  
  2         3  
  2         597  
  0         0  
16              
17             BEGIN {
18 2     2   12 use Config;
  2         4  
  2         99  
19 2     2   10 use constant CAN_SKIP_BIGINTS => $Config{ivsize} >= 8;
  2         4  
  2         364  
20              
21 2     2   2679 if (!CAN_SKIP_BIGINTS) {
22             require Math::BigInt;
23             Math::BigInt->VERSION(1.999808);
24             Math::BigInt->import(try => 'GMP,LTM');
25             }
26             }
27              
28             ### EXPORTED ULID FUNCTIONS
29              
30             sub ulid {
31 4     4 0 1217 return _encode(_ulid(shift));
32             }
33              
34             sub binary_ulid {
35 4     4 0 1118 return _pack(_ulid(shift));
36             }
37              
38             sub ulid_date {
39 0     0 0 0 my $ulid = shift;
40              
41 0 0       0 die "ulid_date() requires DateTime module" unless HAS_DATETIME;
42 0 0       0 die "ulid_date() needs a normal or binary ULID as parameter" unless $ulid;
43              
44 0         0 my ($ts, $rand) = _ulid($ulid);
45              
46 0         0 return DateTime->from_epoch(epoch => _unfix_ts($ts));
47             }
48              
49             sub ulid_to_uuid {
50 1 50   1 0 4 my $ulid = shift or die "Need ULID to convert";
51 1         3 my $bin = _pack(_ulid($ulid));
52 1         3 return _uuid_bin2str($bin)
53             }
54              
55             sub uuid_to_ulid {
56 3 50   3 0 298 my $uuid = shift or die "Need UUID to convert";
57 3         7 my $bin_uuid = _uuid_str2bin($uuid);
58 3         7 return _encode(_ulid($bin_uuid));
59             }
60              
61             ### HELPER FUNCTIONS
62              
63             sub _uuid_bin2str {
64 1     1   2 my $uuid = shift;
65              
66 1 50       3 return $uuid if length($uuid) == 36;
67 1 50       2 die "Invalid uuid" unless length $uuid == 16;
68 1         3 my @offsets = (4, 2, 2, 2, 6);
69              
70             return join(
71             '-',
72 5         32 map { unpack 'H*', $_ }
73 1         2 map { substr $uuid, 0, $_, ''}
  5         10  
74             @offsets);
75             }
76              
77             sub _uuid_str2bin {
78 3     3   5 my $uuid = shift;
79              
80 3 50       7 return $uuid if length $uuid == 16;
81 3         15 $uuid =~ s/-//g;
82              
83 3         14 return pack 'H*', $uuid;
84             }
85              
86             sub _ulid {
87 12     12   21 my $arg = shift;
88 12         16 my $ts;
89              
90 12 100       29 if ($arg) {
91 11 50 33     36 if (ref $arg && $arg->isa('DateTime')) {
    100          
92 0         0 $ts = $arg->hires_epoch;
93             }
94             elsif (length($arg) == 16) {
95 6         13 return _unpack($arg);
96             }
97             else {
98 5         10 $arg = _normalize($arg);
99 5 50       12 die "Invalid ULID supplied: wrong length" unless length($arg) == 26;
100 5         8 return _decode($arg);
101             }
102             }
103              
104 1   33     10 return (_fix_ts($ts || time()), random_bytes(10));
105             }
106              
107             sub _pack {
108 5     5   10 my ($ts, $rand) = @_;
109 5         9 return _zero_pad($ts, 6, "\x00") . _zero_pad($rand, 10, "\x00");
110             }
111              
112             sub _unpack {
113 6     6   20 my ($ts, $rand) = unpack 'a6a10', shift;
114 6         17 return ($ts, $rand);
115             }
116              
117             sub _fix_ts {
118 1     1   2 my $ts = shift;
119              
120 1         2 if (CAN_SKIP_BIGINTS) {
121 1         3 $ts *= 1000;
122 1         11 return pack 'Nn', int($ts / (2 << 15)), $ts % (2 << 15);
123             } else {
124             $ts .= '000';
125             $ts =~ s/\.(\d{3}).*$/$1/;
126             return Math::BigInt->new($ts)->to_bytes;
127             }
128             }
129              
130             sub _unfix_ts {
131 0     0   0 my $ts = shift;
132              
133 0         0 if (CAN_SKIP_BIGINTS) {
134 0         0 my ($high, $low) = unpack 'Nn', $ts;
135 0         0 return ($high * (2 << 15) + $low) / 1000;
136             } else {
137             $ts = Math::BigInt->from_bytes($ts);
138             $ts =~ s/(\d{3})$/.$1/;
139             return $ts;
140             }
141             }
142              
143             sub _encode {
144 7     7   142 my ($ts, $rand) = @_;
145 7         14 return sprintf('%010s%016s', _encode_b32($ts), _encode_b32($rand));
146             }
147              
148             sub _decode {
149 5     5   20 my ($ts, $rand) = map { _decode_b32($_) } unpack 'A10A16', shift;
  10         18  
150 5         17 return ($ts, $rand);
151             }
152              
153             sub _zero_pad {
154 34     34   60 my ($value, $mul, $char) = @_;
155 34   100     101 $char ||= '0';
156              
157 34         55 my $padded = length($value) % $mul;
158 34 100       115 return $value if $padded == 0;
159              
160 12         39 my $padding = substr $value, 0, $padded, '';
161              
162 12 100       51 return $value if $padding eq $char x $padded;
163 2         7 return $char x ($mul - $padded) . $padding . $value;
164             }
165              
166             ### BASE32 ENCODER / DECODER
167              
168             my $ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
169              
170             my %ALPHABET_MAP = do {
171             my $num = 0;
172             map { $_ => substr sprintf('0000%b', $num++), -5 } split //, $ALPHABET;
173             };
174              
175             my %ALPHABET_MAP_REVERSE = map { $ALPHABET_MAP{$_} => $_ } keys %ALPHABET_MAP;
176              
177             sub _normalize {
178 5     5   11 my $s = uc(shift);
179 5         23 my $re = "[^$ALPHABET]";
180              
181 5         45 $s =~ s/$re//g;
182 5         12 return $s;
183             }
184              
185             sub _encode_b32 {
186 14     14   36 my $bits = _zero_pad(unpack('B*', shift), 5);
187 14         22 my $len = length $bits;
188              
189 14         17 my $result = '';
190 14         28 for (my $i = 0; $i < $len; $i += 5) {
191 177         330 $result .= $ALPHABET_MAP_REVERSE{substr $bits, $i, 5};
192             }
193 14         72 return $result;
194             }
195              
196             sub _decode_b32 {
197 10     10   34 my $encoded = join '', map { $ALPHABET_MAP{$_} } split //, uc shift;
  130         208  
198 10         31 return pack 'B*', _zero_pad($encoded, 8);
199             }
200              
201             1;
202              
203             __END__