File Coverage

blib/lib/Hashids.pm
Criterion Covered Total %
statement 118 118 100.0
branch 35 42 83.3
condition 8 9 88.8
subroutine 13 13 100.0
pod 6 7 85.7
total 180 189 95.2


line stmt bran cond sub pod time code
1             package Hashids;
2              
3 5     5   381966 use Carp 'croak';
  5         44  
  5         247  
4 5     5   2530 use POSIX 'ceil';
  5         30834  
  5         26  
5 5     5   8765 use Hashids::Util ':all';
  5         20  
  5         731  
6 5     5   2181 use Moo;
  5         27315  
  5         29  
7 5     5   6113 use namespace::clean;
  5         13  
  5         38  
8              
9             our $VERSION = "1.001012";
10              
11             has salt => ( is => 'ro', default => '' );
12              
13             has minHashLength => (
14             is => 'ro',
15             isa => sub {
16             croak "$_[0] must be a positive number" unless $_[0] =~ /^[0-9]+$/;
17             },
18             default => 0
19             );
20              
21             has alphabet => (
22             is => 'rwp',
23             isa => sub {
24             local $_ = shift;
25             croak "$_ must not have spaces" if /\s/;
26             croak "$_ must contain at least 16 characters" if 16 > length;
27             my %u;
28             croak "$_ must contain unique characters"
29             if any { $u{$_}++ } split //;
30             },
31             default => sub { join '' => 'a' .. 'z', 'A' .. 'Z', 1 .. 9, 0 }
32             );
33              
34             has chars => ( is => 'rwp', init_arg => undef, default => sub { [] } );
35              
36             has seps => (
37             is => 'rwp',
38             init_arg => undef,
39             default => sub {
40             my @seps = qw(c f h i s t u);
41             [ @seps, map {uc} @seps ];
42             },
43             );
44              
45             has guards => ( is => 'rwp', init_arg => undef, default => sub { [] } );
46              
47             around BUILDARGS => sub {
48             my ( $orig, $class, @args ) = @_;
49             unshift @args, 'salt' if @args % 2 == 1;
50              
51             $class->$orig(@args);
52             };
53              
54             sub BUILD {
55 24     24 0 119 my $self = shift;
56              
57 24 100       140 croak "salt must be shorter than or of equal length to alphabet"
58             if length $self->salt > length $self->alphabet;
59              
60 23         657 my @alphabet = split // => $self->alphabet;
61 23         49 my ( @seps, @guards );
62              
63 23         45 my $sepDiv = 3.5;
64 23         35 my $guardDiv = 12;
65              
66             # seps should contain only chars present in alphabet;
67             # alphabet should not contain seps
68 23         37 for my $sep ( @{ $self->seps } ) {
  23         69  
69 322 100   5696   1179 push @seps, $sep if any {/$sep/} @alphabet;
  5696         16443  
70 322         858 @alphabet = grep { !/$sep/ } @alphabet;
  16219         32117  
71             }
72              
73 23         122 @seps = consistent_shuffle( \@seps, $self->salt );
74              
75 23 100 66     167 if ( !@seps || ( @alphabet / @seps ) > $sepDiv ) {
76 1         6 my $sepsLength = ceil( @alphabet / $sepDiv );
77 1 50       5 $sepsLength++ if $sepsLength == 1;
78 1 50       5 if ( $sepsLength > @seps ) {
79 1         5 push @seps => splice @alphabet, 0, $sepsLength - @seps;
80             }
81             }
82              
83 23         90 @alphabet = consistent_shuffle( \@alphabet, $self->salt );
84 23         207 my $guardCount = ceil( @alphabet / $guardDiv );
85              
86             @guards
87 23 50       104 = @alphabet < 3
88             ? splice @seps, 0, $guardCount
89             : splice @alphabet, 0, $guardCount;
90              
91 23         97 $self->_set_chars( \@alphabet );
92 23         60 $self->_set_seps( \@seps );
93 23         230 $self->_set_guards( \@guards );
94             }
95              
96             sub encode_hex {
97 2     2 1 918 my ( $self, $str ) = @_;
98              
99 2 100       18 return '' unless $str =~ /^[0-9a-fA-F]+$/;
100              
101 1         2 my @num;
102 1         7 push @num, '1' . substr $str, 0, 11, '' while $str;
103              
104 1         4 @num = map { bignum(0)->from_hex($_) } @num;
  1         5  
105              
106 1         394 $self->encode(@num);
107             }
108              
109             sub decode_hex {
110 2     2 1 6 my ( $self, $hash ) = @_;
111              
112 2         7 my @res = $self->decode($hash);
113              
114 2 100       14 @res ? join '' => map { substr( bignum($_)->to_hex, 1 ) } @res : '';
  1         3  
115             }
116              
117             sub encrypt {
118 4     4 1 15 shift->encode(@_);
119             }
120              
121             sub decrypt {
122 2     2 1 11 shift->decode(shift);
123             }
124              
125             sub encode {
126 2081     2081 1 662356 my ( $self, @num ) = @_;
127              
128 2081 100       5331 return '' unless @num;
129 2079 100 100     3811 map { return '' unless defined and /^[0-9]+$/ } @num;
  2121         16653  
130              
131 2070         3765 my $num = [ map { bignum($_) } @num ];
  2110         10518  
132              
133 2070         279949 my @alphabet = @{ $self->chars };
  2070         18977  
134 2070         3442 my @res;
135              
136 2070         4416 my $numHashInt = bignum(0);
137 2070         372720 for my $i ( 0 .. $#$num ) {
138 2110         16965 $numHashInt += $num->[$i] % ( $i + 100 );
139             }
140              
141 2070         485291 my $lottery = $res[0] = $alphabet[ $numHashInt % @alphabet ];
142              
143 2070         414413 for my $i ( 0 .. $#$num ) {
144 2110         6852 my $n = bignum( $num->[$i] );
145 2110         330994 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
146             [ 0 .. @alphabet ];
147              
148 2110         8775 @alphabet = consistent_shuffle( \@alphabet, \@s );
149 2110         6065 my $last = to_alphabet( $n, \@alphabet );
150              
151 2110         7209 push @res => split // => $last;
152              
153 2110 100       14072 if ( $i + 1 < @$num ) {
154 40         128 $n %= ord($last) + $i;
155 40         6450 my $sepsIndex = $n % @{ $self->seps };
  40         163  
156 40         7065 push @res, $self->seps->[$sepsIndex];
157             }
158             }
159              
160 2070 100       6843 if ( @res < $self->minHashLength ) {
161 18         52 my $guards = $self->guards;
162 18         54 my $guardIndex = ( $numHashInt + ord $res[0] ) % @$guards;
163 18         6069 my $guard = $guards->[$guardIndex];
164              
165 18         435 unshift @res, $guard;
166              
167 18 50       74 if ( @res < $self->minHashLength ) {
168 18         46 $guardIndex = ( $numHashInt + ord $res[2] ) % @$guards;
169 18         5724 $guard = $guards->[$guardIndex];
170              
171 18         452 push @res, $guard;
172             }
173             }
174              
175 2070         5099 my $halfLength = int @alphabet / 2;
176 2070         4882 while ( @res < $self->minHashLength ) {
177 18         60 @alphabet = consistent_shuffle( \@alphabet, \@alphabet );
178 18         178 @res = (
179             @alphabet[ $halfLength .. $#alphabet ],
180             @res, @alphabet[ 0 .. $halfLength - 1 ]
181             );
182              
183 18 50       69 if ( ( my $excess = @res - $self->minHashLength ) > 0 ) {
184 18         124 @res = splice @res, int $excess / 2, $self->minHashLength;
185             }
186             }
187              
188 2070         16301 join '' => @res;
189             }
190              
191             sub decode {
192 1031     1031 1 6374 my ( $self, $hash ) = @_;
193              
194 1031 100       2165 return unless $hash;
195 1030 50       2306 return unless defined wantarray;
196              
197 1030         1677 my $res = [];
198 1030         1570 my $orig = $hash;
199              
200 1030         2035 my $guard = join '|', map {quotemeta} @{ $self->guards };
  4114         8278  
  1030         2777  
201 1030         5514 my @hash = grep { $_ ne '' } split /$guard/ => $hash;
  1035         3360  
202 1030 100 100     4690 my $i = ( @hash == 3 || @hash == 2 ) ? 1 : 0;
203              
204 1030 50       2567 return unless defined( $hash = $hash[$i] );
205 1030         2042 my $lottery = substr $hash, 0, 1;
206 1030         1701 $hash = substr $hash, 1;
207              
208 1030         1403 my $sep = join '|', @{ $self->seps };
  1030         3931  
209 1030         3698 @hash = grep { $_ ne '' } split /$sep/ => $hash;
  1051         2894  
210              
211 1030         1708 my @alphabet = @{ $self->chars };
  1030         7384  
212 1030         2162 for my $part (@hash) {
213 1051         9737 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
214             [ 0 .. @alphabet ];
215              
216 1051         4122 @alphabet = consistent_shuffle( \@alphabet, \@s );
217 1051         3102 push @$res => from_alphabet( $part, \@alphabet );
218             }
219              
220 1030 100       29998 return unless $self->Hashids::encode(@$res) eq $orig;
221              
222 1029 100       9127 wantarray ? @$res : @$res == 1 ? $res->[0] : $res;
    100          
223             }
224              
225             1;
226             __END__