File Coverage

blib/lib/Data/ULID.pm
Criterion Covered Total %
statement 87 98 88.7
branch 15 26 57.6
condition 3 8 37.5
subroutine 25 27 92.5
pod 0 5 0.0
total 130 164 79.2


line stmt bran cond sub pod time code
1             package Data::ULID;
2              
3 2     2   69906 use strict;
  2         15  
  2         60  
4 2     2   10 use warnings;
  2         3  
  2         79  
5              
6             our $VERSION = '1.2.1';
7              
8 2     2   11 use base qw(Exporter);
  2         4  
  2         349  
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   1040 use Time::HiRes qw/time/;
  2         2753  
  2         8  
13 2     2   1287 use Crypt::PRNG qw/random_bytes/;
  2         6709  
  2         153  
14              
15 2     2   17 use constant HAS_DATETIME => eval { require DateTime; 1 };
  2         5  
  2         3  
  2         531  
  0         0  
16              
17             BEGIN {
18 2     2   11 use Config;
  2         3  
  2         118  
19 2     2   10 use constant CAN_SKIP_BIGINTS => $Config{ivsize} >= 8;
  2         4  
  2         366  
20              
21 2     2   2709 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 1232 return _encode(_ulid(shift));
32             }
33              
34             sub binary_ulid {
35 4     4 0 1115 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         2 my $bin = _pack(_ulid($ulid));
52 1         3 return _uuid_bin2str($bin)
53             }
54              
55             sub uuid_to_ulid {
56 3 50   3 0 295 my $uuid = shift or die "Need UUID to convert";
57 3         6 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       2 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         29 map { unpack 'H*', $_ }
73 1         2 map { substr $uuid, 0, $_, ''}
  5         9  
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         15 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       27 if ($arg) {
91 11 50 33     37 if (ref $arg && $arg->isa('DateTime')) {
    100          
92 0         0 $ts = $arg->hires_epoch;
93             }
94             elsif (length($arg) == 16) {
95 6         10 return _unpack($arg);
96             }
97             else {
98 5         9 $arg = _normalize($arg);
99 5 50       12 die "Invalid ULID supplied: wrong length" unless length($arg) == 26;
100 5         10 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   7 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         20 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         4 $ts = int($ts * 1000);
122 1         9 return pack 'Nn', $ts >> 16, $ts & 0xffff;
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 << 16) + $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   138 my ($ts, $rand) = @_;
145 7         13 return sprintf('%010s%016s', _encode_b32($ts), _encode_b32($rand));
146             }
147              
148             sub _decode {
149 5     5   18 my ($ts, $rand) = map { _decode_b32($_) } unpack 'A10A16', shift;
  10         18  
150 5         19 return ($ts, $rand);
151             }
152              
153             sub _zero_pad {
154             # this function is used a lot. Keep it as lean as possible
155             # my ($value, $character_multiplier, $padding_character) = @_;
156 34     34   40 my $value = shift;
157              
158 34         53 my $padded = length($value) % $_[0];
159 34 100       96 return $value if $padded == 0;
160              
161 12   50     51 $_[1] ||= 0;
162 12         28 my $padding = substr $value, 0, $padded, '';
163              
164 12 100       63 return $value if $padding eq $_[1] x $padded;
165 2         8 return $_[1] x ($_[0] - $padded) . $padding . $value;
166             }
167              
168             ### BASE32 ENCODER / DECODER
169              
170             my $ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
171              
172             my %ALPHABET_MAP = do {
173             my $num = 0;
174             map { $_ => substr sprintf('0000%b', $num++), -5 } split //, $ALPHABET;
175             };
176              
177             my %ALPHABET_MAP_REVERSE = map { $ALPHABET_MAP{$_} => $_ } keys %ALPHABET_MAP;
178              
179             sub _normalize {
180 5     5   11 my $s = uc(shift);
181 5         10 my $re = "[^$ALPHABET]";
182              
183 5         34 $s =~ s/$re//g;
184 5         11 return $s;
185             }
186              
187             sub _encode_b32 {
188 14     14   34 my $bits = _zero_pad(unpack('B*', shift), 5);
189 14         24 my $len = length $bits;
190              
191 14         17 my $result = '';
192 14         29 for (my $i = 0; $i < $len; $i += 5) {
193 177         324 $result .= $ALPHABET_MAP_REVERSE{substr $bits, $i, 5};
194             }
195 14         61 return $result;
196             }
197              
198             sub _decode_b32 {
199 10     10   47 my $encoded = join '', map { $ALPHABET_MAP{$_} } split //, uc shift;
  130         205  
200 10         31 return pack 'B*', _zero_pad($encoded, 8);
201             }
202              
203             1;
204              
205             __END__