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