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   380053 use Carp 'croak';
  5         51  
  5         251  
4 5     5   2483 use POSIX 'ceil';
  5         30783  
  5         26  
5 5     5   8646 use Hashids::Util ':all';
  5         15  
  5         762  
6 5     5   2188 use Moo;
  5         26793  
  5         26  
7 5     5   6089 use namespace::clean;
  5         10  
  5         35  
8              
9             our $VERSION = "1.001011";
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 99 my $self = shift;
56              
57 24 100       145 croak "salt must be shorter than or of equal length to alphabet"
58             if length $self->salt > length $self->alphabet;
59              
60 23         664 my @alphabet = split // => $self->alphabet;
61 23         46 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         32 for my $sep ( @{ $self->seps } ) {
  23         71  
69 322 100   5696   1158 push @seps, $sep if any {/$sep/} @alphabet;
  5696         15907  
70 322         838 @alphabet = grep { !/$sep/ } @alphabet;
  16219         30709  
71             }
72              
73 23         118 @seps = consistent_shuffle( \@seps, $self->salt );
74              
75 23 100 66     155 if ( !@seps || ( @alphabet / @seps ) > $sepDiv ) {
76 1         7 my $sepsLength = ceil( @alphabet / $sepDiv );
77 1 50       5 $sepsLength++ if $sepsLength == 1;
78 1 50       4 if ( $sepsLength > @seps ) {
79 1         5 push @seps => splice @alphabet, 0, $sepsLength - @seps;
80             }
81             }
82              
83 23         92 @alphabet = consistent_shuffle( \@alphabet, $self->salt );
84 23         171 my $guardCount = ceil( @alphabet / $guardDiv );
85              
86             @guards
87 23 50       92 = @alphabet < 3
88             ? splice @seps, 0, $guardCount
89             : splice @alphabet, 0, $guardCount;
90              
91 23         85 $self->_set_chars( \@alphabet );
92 23         64 $self->_set_seps( \@seps );
93 23         220 $self->_set_guards( \@guards );
94             }
95              
96             sub encode_hex {
97 2     2 1 941 my ( $self, $str ) = @_;
98              
99 2 100       16 return '' unless $str =~ /^[0-9a-fA-F]+$/;
100              
101 1         3 my @num;
102 1         10 push @num, '1' . substr $str, 0, 11, '' while $str;
103              
104 1         3 @num = map { bignum(0)->from_hex($_) } @num;
  1         8  
105              
106 1         369 $self->encode(@num);
107             }
108              
109             sub decode_hex {
110 2     2 1 17 my ( $self, $hash ) = @_;
111              
112 2         11 my @res = $self->decode($hash);
113              
114 2 100       11 @res ? join '' => map { substr( bignum($_)->to_hex, 1 ) } @res : '';
  1         3  
115             }
116              
117             sub encrypt {
118 4     4 1 27 shift->encode(@_);
119             }
120              
121             sub decrypt {
122 2     2 1 8 shift->decode(shift);
123             }
124              
125             sub encode {
126 2081     2081 1 662309 my ( $self, @num ) = @_;
127              
128 2081 100       5422 return '' unless @num;
129 2079 100 100     3926 map { return '' unless defined and /^[0-9]+$/ } @num;
  2121         16303  
130              
131 2070         3804 my $num = [ map { bignum($_) } @num ];
  2110         10707  
132              
133 2070         281715 my @alphabet = @{ $self->chars };
  2070         19027  
134 2070         3598 my @res;
135              
136 2070         4776 my $numHashInt = bignum(0);
137 2070         375509 for my $i ( 0 .. $#$num ) {
138 2110         17061 $numHashInt += $num->[$i] % ( $i + 100 );
139             }
140              
141 2070         488908 my $lottery = $res[0] = $alphabet[ $numHashInt % @alphabet ];
142              
143 2070         419699 for my $i ( 0 .. $#$num ) {
144 2110         6739 my $n = bignum( $num->[$i] );
145 2110         332064 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
146             [ 0 .. @alphabet ];
147              
148 2110         8616 @alphabet = consistent_shuffle( \@alphabet, \@s );
149 2110         5736 my $last = to_alphabet( $n, \@alphabet );
150              
151 2110         6603 push @res => split // => $last;
152              
153 2110 100       13857 if ( $i + 1 < @$num ) {
154 40         129 $n %= ord($last) + $i;
155 40         6467 my $sepsIndex = $n % @{ $self->seps };
  40         141  
156 40         7123 push @res, $self->seps->[$sepsIndex];
157             }
158             }
159              
160 2070 100       7068 if ( @res < $self->minHashLength ) {
161 18         50 my $guards = $self->guards;
162 18         50 my $guardIndex = ( $numHashInt + ord $res[0] ) % @$guards;
163 18         5931 my $guard = $guards->[$guardIndex];
164              
165 18         470 unshift @res, $guard;
166              
167 18 50       56 if ( @res < $self->minHashLength ) {
168 18         45 $guardIndex = ( $numHashInt + ord $res[2] ) % @$guards;
169 18         5913 $guard = $guards->[$guardIndex];
170              
171 18         443 push @res, $guard;
172             }
173             }
174              
175 2070         4669 my $halfLength = int @alphabet / 2;
176 2070         4969 while ( @res < $self->minHashLength ) {
177 18         50 @alphabet = consistent_shuffle( \@alphabet, \@alphabet );
178 18         180 @res = (
179             @alphabet[ $halfLength .. $#alphabet ],
180             @res, @alphabet[ 0 .. $halfLength - 1 ]
181             );
182              
183 18 50       74 if ( ( my $excess = @res - $self->minHashLength ) > 0 ) {
184 18         106 @res = splice @res, int $excess / 2, $self->minHashLength;
185             }
186             }
187              
188 2070         15511 join '' => @res;
189             }
190              
191             sub decode {
192 1031     1031 1 6195 my ( $self, $hash ) = @_;
193              
194 1031 100       2179 return unless $hash;
195 1030 50       2074 return unless defined wantarray;
196              
197 1030         1637 my $res = [];
198 1030         1733 my $orig = $hash;
199              
200 1030         1346 my $guard = join '|', map {quotemeta} @{ $self->guards };
  4114         7969  
  1030         2645  
201 1030         5412 my @hash = grep { $_ ne '' } split /$guard/ => $hash;
  1035         3148  
202 1030 100 100     4631 my $i = ( @hash == 3 || @hash == 2 ) ? 1 : 0;
203              
204 1030 50       2621 return unless defined( $hash = $hash[$i] );
205 1030         2028 my $lottery = substr $hash, 0, 1;
206 1030         1744 $hash = substr $hash, 1;
207              
208 1030         1418 my $sep = join '|', @{ $self->seps };
  1030         3573  
209 1030         3863 @hash = grep { $_ ne '' } split /$sep/ => $hash;
  1051         3041  
210              
211 1030         1651 my @alphabet = @{ $self->chars };
  1030         7699  
212 1030         1969 for my $part (@hash) {
213 1051         9748 my @s = ( $lottery, split( // => $self->salt ), @alphabet )
214             [ 0 .. @alphabet ];
215              
216 1051         4366 @alphabet = consistent_shuffle( \@alphabet, \@s );
217 1051         3006 push @$res => from_alphabet( $part, \@alphabet );
218             }
219              
220 1030 100       29250 return unless $self->Hashids::encode(@$res) eq $orig;
221              
222 1029 100       9379 wantarray ? @$res : @$res == 1 ? $res->[0] : $res;
    100          
223             }
224              
225             1;
226             __END__