File Coverage

blib/lib/Digest/ssdeep.pm
Criterion Covered Total %
statement 135 144 93.7
branch 31 36 86.1
condition 12 17 70.5
subroutine 22 23 95.6
pod 4 4 100.0
total 204 224 91.0


line stmt bran cond sub pod time code
1             package Digest::ssdeep;
2              
3 3     3   147630 use warnings;
  3         7  
  3         196  
4 3     3   17 use strict;
  3         5  
  3         106  
5 3     3   16 use Carp;
  3         8  
  3         277  
6 3     3   3176 use Text::WagnerFischer qw/distance/;
  3         2707  
  3         250  
7 3     3   19 use List::Util qw/max/;
  3         7  
  3         348  
8              
9 3     3   3429 use version;
  3         10035  
  3         18  
10             our $VERSION = qv('0.9.3');
11              
12             BEGIN {
13 3     3   16 require Exporter;
14 3     3   333 use vars qw(@ISA @EXPORT_OK);
  3         5  
  3         180  
15 3         46 @ISA = qw(Exporter);
16 3         345 @EXPORT_OK = qw(
17             ssdeep_hash
18             ssdeep_hash_file
19             ssdeep_compare
20             ssdeep_dump_last
21             );
22             }
23              
24 3     3   16 use constant FNV_PRIME => 0x01000193;
  3         7  
  3         211  
25 3     3   35 use constant FNV_INIT => 0x28021967;
  3         17  
  3         112  
26 3     3   14 use constant MAX_LENGTH => 64;
  3         5  
  3         716  
27              
28             # Weights:
29             # same = 0
30             # insertion/deletion = 1
31             # mismatch = 2
32             # swap = N/A (should be 5)
33             $Text::WagnerFischer::REFC = [ 0, 1, 2 ];
34              
35             my @b64 = split '',
36             "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
37             my @DEBUG_LAST;
38              
39             my @last7chars; # will use character 7 places before
40             { # begin rolling hash internals
41              
42             my $roll_h1; # rolling hash internal
43             my $roll_h2; # rolling hash internal
44             my $roll_h3; # rolling hash internal
45              
46             # Resets the roll hash internal status
47             sub _reset_rollhash {
48 37     37   173 @last7chars =
49             ( 0, 0, 0, 0, 0, 0, 0 ); # will use character 7 places before
50 37         46 $roll_h1 = 0;
51 37         61 $roll_h2 = 0;
52 37         59 $roll_h3 = 0;
53             }
54              
55             # Updates rolling_hash's internal state and return the rolling_hash value.
56             # Parameters: the next character.
57             # Returns: the actual rolling hash value
58             sub _roll_hash {
59 522683     522683   532979 my $char = shift;
60 522683         572572 my $char7bf = shift @last7chars;
61              
62 522683         612422 push @last7chars, $char;
63              
64 522683         604934 $roll_h2 += 7 * $char - $roll_h1;
65 522683         541506 $roll_h1 += $char - $char7bf;
66              
67 522683         491504 $roll_h3 <<= 5; # 5*7 = 35 (so it vanish after 7 iterations)
68 522683         503172 $roll_h3 &= 0xffffffff;
69 522683         507575 $roll_h3 ^= $char;
70              
71             #printf("c=%d cAnt=%d H1=%u H2=%u H3=%u\n",
72             # $char, $char7bf,
73             # $roll_h1, $roll_h2, $roll_h3);
74              
75 522683         773369 return $roll_h1 + $roll_h2 + $roll_h3;
76             }
77              
78             } # end rolling hash internals
79              
80             # In-place updates the FNV hash using the new character
81             # _update_fnv($fnvhash, $newchar);
82             sub _update_fnv {
83 3     3   3098 use integer; # we need integer overflow in multiplication
  3         31  
  3         16  
84 1045366     1045366   1002002 $_[0] *= FNV_PRIME;
85 1045366         1005640 $_[0] &= 0xffffffff;
86 1045366         1106062 $_[0] ^= $_[1];
87 3     3   184 no integer;
  3         5  
  3         11  
88             }
89              
90             # Calculates initial blocksize
91             # Parameter: the length of the whole data
92             sub _calc_initbs {
93 32     32   53 my $length = shift;
94              
95             # MAX_LENGTH * bs < length
96             # MAX_LENGTH * 3 * 2 * 2 * 2 * ... < length
97             #my $n = int(log($length / (MAX_LENGTH * 3)) / log(2));
98             #my $bs = 3 * 2**$n;
99 32         49 my $bs = 3;
100 32         223 $bs *= 2 while ( $bs * MAX_LENGTH < $length );
101              
102 32 100       115 return $bs > 3 ? $bs : 3;
103             }
104              
105             # Calculates the ssdeep fuzzy hash of a string
106             # Parameters: the string
107             # Returns: the fuzzy hash in string or array
108             sub ssdeep_hash {
109 32     32 1 370637 my $string = shift;
110              
111 32 50       127 return unless defined $string;
112              
113 32         271 my $bs = _calc_initbs( length $string );
114 32         1459 @DEBUG_LAST = ();
115              
116 32         43 my $hash1;
117             my $hash2;
118              
119 32         208 while (1) {
120 37         96 _reset_rollhash();
121 37         44 my $fnv1 = FNV_INIT; # traditional hash blocksize
122 37         42 my $fnv2 = FNV_INIT; # traditional hash 2*blocksize
123              
124 37         262 $hash1 = '';
125 37         44 $hash2 = '';
126              
127 37         110 for my $i ( 0 .. length($string) - 1 ) {
128 522683         648079 my $c = ord( substr( $string, $i, 1 ) );
129              
130             #printf("c: %u, H1=%x\tH2=%x\n", $c, $fnv1, $fnv2);
131              
132 522683         721671 my $h = _roll_hash($c);
133 522683         795140 _update_fnv( $fnv1, $c ); # blocksize FNV hash
134 522683         743176 _update_fnv( $fnv2, $c ); # 2* blocksize FNV hash
135              
136 522683 100 100     1262209 if ( $h % $bs == ( $bs - 1 ) and length $hash1 < MAX_LENGTH - 1 ) {
137              
138             #printf "Hash $h Trigger 1 at $i\n";
139 1296         2670 my $b64char = $b64[ $fnv1 & 63 ];
140 1296         1678 $hash1 .= $b64char;
141              
142 1296         9280 push @DEBUG_LAST,
143             [ 1, $i + 1, join( '|', @last7chars ), $fnv1, $b64char ];
144              
145 1296         2342 $fnv1 = FNV_INIT;
146              
147             }
148              
149 522683 100 100     1431342 if ( $h % ( 2 * $bs ) == ( 2 * $bs - 1 )
150             and length $hash2 < MAX_LENGTH / 2 - 1 )
151             {
152              
153             #printf "Hash $h Trigger 2 at $i\n";
154 608         884 my $b64char = $b64[ $fnv2 & 63 ];
155 608         757 $hash2 .= $b64char;
156              
157 608         3161 push @DEBUG_LAST,
158             [ 2, $i + 1, join( '|', @last7chars ), $fnv2, $b64char ];
159              
160 608         1102 $fnv2 = FNV_INIT;
161             }
162              
163             }
164              
165 37         109 $hash1 .= $b64[ $fnv1 & 63 ];
166 37         61 $hash2 .= $b64[ $fnv2 & 63 ];
167              
168 37         272 push @DEBUG_LAST,
169             [
170             1, length($string),
171             join( '|', @last7chars ), $fnv1,
172             $b64[ $fnv1 & 63 ]
173             ];
174              
175 37         203 push @DEBUG_LAST,
176             [
177             2, length($string),
178             join( '|', @last7chars ), $fnv2,
179             $b64[ $fnv2 & 63 ]
180             ];
181              
182 37 100 100     224 last if $bs <= 3 or length $hash1 >= MAX_LENGTH / 2;
183              
184 5         15 $bs = int( $bs / 2 ); # repeat with half blocksize if no enough triggers
185 5 100       17 $bs > 3 or $bs = 3;
186             }
187              
188 32         88 my @outarray = ( $bs, $hash1, $hash2 );
189 32 100       427 return wantarray ? @outarray : join ':', @outarray;
190             }
191              
192             # Convenient function. Slurps file. You should not use it for long files.
193             # You should not use pure perl implementation for long files anyway.
194             # Parameter: filename
195             # Returns: ssdeep hash in string or array format
196             sub ssdeep_hash_file {
197 2     2 1 9963 my $file = shift;
198              
199             # Slurp the file (we can also use File::Slurp
200 2         14 local ($/);
201 2 50       152 open( my $fh, '<', $file ) or return;
202 2         348 my $string = <$fh>;
203 2         36 close $fh;
204              
205 2         8 return ssdeep_hash($string);
206             }
207              
208             # Determines the longest common substring
209             sub _lcss {
210 25     25   69 my $strings = join "\0", @_;
211 25         38 my $lcs = '';
212              
213 25         61 for my $n ( 1 .. length $strings ) {
214 506         934 my $re = "(.{$n})" . '.*\0.*\1' x ( @_ - 1 );
215 506 100       11378 last unless $strings =~ $re;
216 481         975 $lcs = $1;
217             }
218              
219 25         87 return $lcs;
220             }
221              
222             # Calculates how similar two strings are using the Wagner-Fischer package.
223             # Parameters: min_lcs, string A, string B
224             # Returns: the likeliness being 0 totally dissimilar and 100 same string
225             # Returns 0 also if the longest common substring is shorter than min_lcs
226             sub _likeliness {
227 25     25   56 my ( $min_lcs, $a, $b ) = @_;
228              
229 25 100       55 return 0 unless length( _lcss( $a, $b ) ) >= $min_lcs;
230              
231 22         86 my $dist = distance( $a, $b );
232              
233             #$DB::single = 2;
234              
235             # Must follow ssdeep original's code for compatibility
236             # $dist = 100 * $dist / (length($a) + length($b));
237 22         540557 $dist = int( $dist * MAX_LENGTH / ( length($a) + length($b) ) );
238 22         48 $dist = int( 100 * $dist / 64 );
239              
240 22 50       73 $dist > 100 and $dist = 100;
241 22         59 return 100 - $dist;
242             }
243              
244             # We accept hash in both array and scalar format
245             # Parameters: $hashA, $hashB, [$min_lcs]
246             # Parameters: \@hashA, \@hashB, [$min_lcs]
247             # Returns: file matching in %
248             sub ssdeep_compare {
249 19     19 1 11687 my @hashA; # hash = bs:hash1:hash2
250             my @hashB; # hash = bs:hash1:hash2
251 19 100       117 @hashA = ref( $_[0] ) eq 'ARRAY' ? @{ $_[0] } : split ':', $_[0];
  1         2  
252 19 100       99 @hashB = ref( $_[1] ) eq 'ARRAY' ? @{ $_[1] } : split ':', $_[1];
  1         3  
253 19   50     113 my $min_lcs = $_[2] || 7;
254              
255 19 50 33     159 if ( @hashA != 3 or $hashA[0] !~ /\d+/ ) {
256 0         0 carp "Argument 1 is not a ssdeep hash.";
257 0         0 return;
258             }
259              
260 19 50 33     127 if ( @hashB != 3 or $hashB[0] !~ /\d+/ ) {
261 0         0 carp "Argument 2 is not a ssdeep hash.";
262 0         0 return;
263             }
264              
265             # Remove sequences of more than three repeated character
266 19         393 s/(.)\1{3,}/$1/gi for @hashA;
267 19         366 s/(.)\1{3,}/$1/gi for @hashB;
268              
269             # Remove trailing newlines
270 19         84 s/\s+$//gi for @hashA;
271 19         130 s/\s+$//gi for @hashB;
272              
273             #$DB::single = 2;
274              
275 19         34 my $like;
276              
277             # Blocksize comparison
278             # bsA:hash_bsA:hash_2*bsA
279             # bsB:hash_bsB:hash_2*bsB
280 19 100       6631 if ( $hashA[0] == $hashB[0] ) {
    100          
    100          
281              
282             # Compare both hashes
283 7         21 my $like1 = _likeliness( $min_lcs, $hashA[1], $hashB[1] );
284 7         33 my $like2 = _likeliness( $min_lcs, $hashA[2], $hashB[2] );
285 7         47 $like = max( $like1, $like2 );
286             }
287             elsif ( $hashA[0] == 2 * $hashB[0] ) {
288              
289             # Compare hash_bsA with hash_2*bsB
290 6         20 $like = _likeliness( $min_lcs, $hashA[1], $hashB[2] );
291             }
292             elsif ( 2 * $hashA[0] == $hashB[0] ) {
293              
294             # Compare hash_2*bsA with hash_bsB
295 5         18 $like = _likeliness( $min_lcs, $hashA[2], $hashB[1] );
296             }
297             else {
298              
299             # Nothing suitable to compare, sorry
300 1         9 return 0;
301             }
302              
303 18         131 return $like;
304             }
305              
306             # Dump internals information. See help.
307             sub ssdeep_dump_last {
308 0     0 1   my @result;
309 0           for (@DEBUG_LAST) {
310 0           push @result, join ",", @{$_};
  0            
311             }
312 0           return @result;
313             }
314              
315             1; # Magic true value required at end of module
316             __END__