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   410202 use Carp 'croak';
  5         44  
  5         268  
4 5     5   3066 use POSIX 'ceil';
  5         32494  
  5         26  
5 5     5   8949 use Hashids::Util ':all';
  5         14  
  5         872  
6 5     5   2438 use Moo;
  5         28928  
  5         34  
7 5     5   6556 use namespace::clean;
  5         14  
  5         45  
8              
9             our $VERSION = "1.001013";
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       175 croak "salt must be shorter than or of equal length to alphabet"
58             if length $self->salt > length $self->alphabet;
59              
60 23         679 my @alphabet = split // => $self->alphabet;
61 23         61 my ( @seps, @guards );
62              
63 23         40 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         40 for my $sep ( @{ $self->seps } ) {
  23         106  
69 322 100   5696   1215 push @seps, $sep if any {/$sep/} @alphabet;
  5696         16527  
70 322         842 @alphabet = grep { !/$sep/ } @alphabet;
  16219         30936  
71             }
72              
73 23         130 @seps = consistent_shuffle( \@seps, $self->salt );
74              
75 23 100 66     199 if ( !@seps || ( @alphabet / @seps ) > $sepDiv ) {
76 1         7 my $sepsLength = ceil( @alphabet / $sepDiv );
77 1 50       6 $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         111 @alphabet = consistent_shuffle( \@alphabet, $self->salt );
84 23         195 my $guardCount = ceil( @alphabet / $guardDiv );
85              
86             @guards
87 23 50       95 = @alphabet < 3
88             ? splice @seps, 0, $guardCount
89             : splice @alphabet, 0, $guardCount;
90              
91 23         102 $self->_set_chars( \@alphabet );
92 23         68 $self->_set_seps( \@seps );
93 23         209 $self->_set_guards( \@guards );
94             }
95              
96             sub encode_hex {
97 2     2 1 955 my ( $self, $str ) = @_;
98              
99 2 100       17 return '' unless $str =~ /^[0-9a-fA-F]+$/;
100              
101 1         2 my @num;
102 1         9 push @num, '1' . substr $str, 0, 11, '' while $str;
103              
104 1         4 @num = map { bignum(0)->from_hex($_) } @num;
  1         4  
105              
106 1         431 $self->encode(@num);
107             }
108              
109             sub decode_hex {
110 2     2 1 7 my ( $self, $hash ) = @_;
111              
112 2         7 my @res = $self->decode($hash);
113              
114 2 100       11 @res ? join '' => map { substr( bignum($_)->to_hex, 1 ) } @res : '';
  1         4  
115             }
116              
117             sub encrypt {
118 4     4 1 17 shift->encode(@_);
119             }
120              
121             sub decrypt {
122 2     2 1 7 shift->decode(shift);
123             }
124              
125             sub encode {
126 2081     2081 1 676725 my ( $self, @num ) = @_;
127              
128 2081 100       5293 return '' unless @num;
129 2079 100 100     3973 map { return '' unless defined and /^[0-9]+$/ } @num;
  2121         15765  
130              
131 2070         4017 my $num = [ map { bignum($_) } @num ];
  2110         10656  
132              
133 2070         284375 my @alphabet = @{ $self->chars };
  2070         19159  
134 2070         3441 my @res;
135              
136 2070         4809 my $numHashInt = bignum(0);
137 2070         377151 for my $i ( 0 .. $#$num ) {
138 2110         17431 $numHashInt += $num->[$i] % ( $i + 100 );
139             }
140              
141 2070         493076 my $lottery = $res[0] = $alphabet[ $numHashInt % @alphabet ];
142              
143 2070         421406 for my $i ( 0 .. $#$num ) {
144 2110         6914 my $n = bignum( $num->[$i] );
145 2110         334670 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
146             [ 0 .. @alphabet ];
147              
148 2110         8984 @alphabet = consistent_shuffle( \@alphabet, \@s );
149 2110         5854 my $last = to_alphabet( $n, \@alphabet );
150              
151 2110         7001 push @res => split // => $last;
152              
153 2110 100       14034 if ( $i + 1 < @$num ) {
154 40         128 $n %= ord($last) + $i;
155 40         6548 my $sepsIndex = $n % @{ $self->seps };
  40         144  
156 40         7056 push @res, $self->seps->[$sepsIndex];
157             }
158             }
159              
160 2070 100       7010 if ( @res < $self->minHashLength ) {
161 18         45 my $guards = $self->guards;
162 18         60 my $guardIndex = ( $numHashInt + ord $res[0] ) % @$guards;
163 18         6211 my $guard = $guards->[$guardIndex];
164              
165 18         455 unshift @res, $guard;
166              
167 18 50       73 if ( @res < $self->minHashLength ) {
168 18         50 $guardIndex = ( $numHashInt + ord $res[2] ) % @$guards;
169 18         5877 $guard = $guards->[$guardIndex];
170              
171 18         463 push @res, $guard;
172             }
173             }
174              
175 2070         5085 my $halfLength = int @alphabet / 2;
176 2070         4965 while ( @res < $self->minHashLength ) {
177 18         61 @alphabet = consistent_shuffle( \@alphabet, \@alphabet );
178 18         209 @res = (
179             @alphabet[ $halfLength .. $#alphabet ],
180             @res, @alphabet[ 0 .. $halfLength - 1 ]
181             );
182              
183 18 50       75 if ( ( my $excess = @res - $self->minHashLength ) > 0 ) {
184 18         113 @res = splice @res, int $excess / 2, $self->minHashLength;
185             }
186             }
187              
188 2070         15836 join '' => @res;
189             }
190              
191             sub decode {
192 1031     1031 1 6744 my ( $self, $hash ) = @_;
193              
194 1031 100       2278 return unless $hash;
195 1030 50       2377 return unless defined wantarray;
196              
197 1030         1640 my $res = [];
198 1030         1710 my $orig = $hash;
199              
200 1030         1411 my $guard = join '|', map {quotemeta} @{ $self->guards };
  4114         7841  
  1030         2880  
201 1030         5417 my @hash = grep { $_ ne '' } split /$guard/ => $hash;
  1035         3174  
202 1030 100 100     5175 my $i = ( @hash == 3 || @hash == 2 ) ? 1 : 0;
203              
204 1030 50       2618 return unless defined( $hash = $hash[$i] );
205 1030         2053 my $lottery = substr $hash, 0, 1;
206 1030         1680 $hash = substr $hash, 1;
207              
208 1030         1591 my $sep = join '|', @{ $self->seps };
  1030         3415  
209 1030         3742 @hash = grep { $_ ne '' } split /$sep/ => $hash;
  1051         2873  
210              
211 1030         1576 my @alphabet = @{ $self->chars };
  1030         7663  
212 1030         2161 for my $part (@hash) {
213 1051         10353 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
214             [ 0 .. @alphabet ];
215              
216 1051         4359 @alphabet = consistent_shuffle( \@alphabet, \@s );
217 1051         3229 push @$res => from_alphabet( $part, \@alphabet );
218             }
219              
220 1030 100       29800 return unless $self->Hashids::encode(@$res) eq $orig;
221              
222 1029 100       9498 wantarray ? @$res : @$res == 1 ? $res->[0] : $res;
    100          
223             }
224              
225             1;
226             __END__