File Coverage

blib/lib/Ed2k_link.pm
Criterion Covered Total %
statement 54 210 25.7
branch 12 144 8.3
condition 2 51 3.9
subroutine 16 29 55.1
pod 15 15 100.0
total 99 449 22.0


line stmt bran cond sub pod time code
1 2     2   42244 use strict;
  2         4  
  2         120  
2 2     2   14 use warnings;
  2         3  
  2         70  
3 2     2   1495 use utf8;
  2         29  
  2         10  
4              
5             package Ed2k_link;
6             $Ed2k_link::VERSION = '20160412';
7              
8 2     2   113 use Carp ();
  2         4  
  2         37  
9 2     2   10 use File::Basename ();
  2         3  
  2         33  
10 2     2   1238 use URI::Escape ();
  2         3008  
  2         59  
11 2     2   1321 use Encode::Locale ();
  2         31106  
  2         56  
12 2     2   13 use Encode ();
  2         4  
  2         36  
13 2     2   1245 use Digest::MD4 ();
  2         1732  
  2         58  
14 2     2   1408 use Digest::SHA ();
  2         7396  
  2         87  
15              
16             use constant {
17 2         6247 CHUNK_SIZE => 9_728_000,
18             BLOCK_SIZE => 184_320,
19 2     2   14 };
  2         3  
20              
21             =head1 NAME
22              
23             Ed2k_link - module for creating eD2K links and working with them.
24              
25             =head1 VERSION
26              
27             version 20160412
28              
29             =head1 SYNOPSIS
30              
31             use Ed2k_link ();
32              
33             print Ed2k_link -> from_file( 'c:\\temp\\new_movie.mkv' ) -> link( 'h' ) . "\n";
34              
35             my $emule = Ed2k_link -> from_file( 'eMule0.49c.zip' ) or die 'something wrong with file!');
36              
37             my $sources = Ed2k_link -> from_link( 'ed2k://|file|eMule0.49c.zip|2868871|0F88EEFA9D8AD3F43DABAC9982D2450C|/' ) or die 'incorrect link!';
38              
39             $sources -> from_link( 'ed2k://|file|eMule0.49c-Sources.zip|5770302|195B6D8286BF184C3CC0665148D746CF|/' ) or die 'incorrect link!';
40              
41             print $emule -> link( 'h' ) if $emule -> filesize <= 10 * 1024 * 1024, "\n";
42              
43             if ( Ed2k_link -> equal( $emule, $sources ) {
44             printf "files %s and %s are equal\n";
45             $emule -> filename,
46             $sources -> filename;
47             }
48              
49             print Ed2k_link -> from_file( '/somethere/cool_file.txt' ) -> link('hp');
50              
51             =head1 DESCRIPTION
52              
53             Ed2k_link module for creating eD2K links from files with correct hash, AICH hash and complete hashset fields.
54             Also it can work with already created links (e. g. from textfile).
55              
56             =cut
57              
58             sub _encode_base32 {
59 0     0   0 my %bits_to_char = qw# 00000 A 00001 B 00010 C 00011 D 00100 E 00101 F 00110 G 00111 H
60             01000 I 01001 J 01010 K 01011 L 01100 M 01101 N 01110 O 01111 P
61             10000 Q 10001 R 10010 S 10011 T 10100 U 10101 V 10110 W 10111 X
62             11000 Y 11001 Z 11010 2 11011 3 11100 4 11101 5 11110 6 11111 7
63             #;
64 0         0 my ($source, $bits, $res) = shift;
65 0         0 $bits .= unpack('B*', substr($source, $_, 1)) for 0 .. length($source) - 1;
66             # generally $bits length could be not 40 * k and there has to be padding. not our case
67 0         0 $res .= $bits_to_char{$&} while $bits =~ m/.{5}/g;
68 0         0 $res;
69             }
70              
71             sub _define_base_trees_orientation { # l/r, array_ref, start_idx, end_idx
72 0 0   0   0 if ($_[2] - $_[3] >= 0) {
    0          
73 0         0 $_[1][$_[2]] = $_[0];
74             } elsif ($_[2] + 1 == $_[3]) {
75 0         0 $_[1][$_[2]] = 'l';
76 0         0 $_[1][$_[3]] = 'r';
77             } else {
78 0         0 my $med = sprintf("%d", ($_[2] + $_[3]) / 2);
79 0 0 0     0 -- $med if $_[ 0 ] eq 'r' && $_[ 2 ] + $_[ 3 ] == $med * 2;
80 0         0 &_define_base_trees_orientation( 'l', $_[ 1 ], $_[ 2 ], $med );
81 0         0 &_define_base_trees_orientation( 'r', $_[ 1 ], ++ $med, $_[ 3 ] );
82             }
83             }
84              
85             sub _get_root_hash { # l/r, array_ref, start_idx, end_idx
86 0     0   0 my $med = $_[3];
87 0 0       0 if ($_[2] - $_[3] >= 0) {
    0          
88 0         0 return;
89             } elsif ($_[3] - $_[2] > 1) {
90 0         0 $med = sprintf("%d", ($_[2] + $_[3]) / 2);
91 0 0 0     0 -- $med if $_[ 0 ] eq 'r' && $_[ 2 ] + $_[ 3 ] == $med * 2;
92 0         0 &_get_root_hash( 'l',
93             $_[ 1 ],
94             $_[ 2 ],
95             $med
96             );
97 0         0 &_get_root_hash( 'r',
98             $_[ 1 ],
99             ++ $med,
100             $_[ 3 ]
101             );
102             }
103              
104 0         0 $_[ 1 ] -> [ $_[ 2 ] ] = Digest::SHA::sha1( $_[ 1 ] -> [ $_[ 2 ] ],
105             $_[ 1 ] -> [ $med ]
106             );
107             }
108              
109             =head1 CLASS METHODS
110              
111             =head2 from_file
112              
113             Can be called as class or instance method:
114              
115             my $t = Ed2k_link -> from_file( 'file_1.txt' ) or die 'error!';
116              
117             $t -> from_file( 'file_2.txt' ) or die 'error!';
118              
119             Creates all fields of eD2K link including hash, AICH hashset, complete hashset.
120              
121             Filename should be a character string (as opposed to octet string). In case of any error returns undef and object doesn't hold any link information.
122              
123             Sets Reliable flag to true.
124              
125             =cut
126              
127             sub from_file {
128 0     0 1 0 my $either = shift;
129 0 0       0 %$either = () if ref $either;
130 0         0 my $file = shift; # string of characters (not an octet stream)
131 0 0       0 return undef unless defined $file;
132              
133             # file must exist and be not empty!
134 0         0 my $filename_to_access = Encode::encode( locale_fs => $file );
135 0 0 0     0 return undef unless -f $filename_to_access && -s _;
136              
137 0         0 my $self = { path_to_file => $file,
138             size => -s _,
139             filename => File::Basename::fileparse( $file ),
140             };
141              
142             # emule doesn't escape #[]@$&+,;=
143 0         0 $self -> {escaped_filename} = URI::Escape::uri_escape_utf8( $self -> {filename}, '^A-Za-z0-9\-_.!~*\'()#&+,;=' );
144             # []@$
145 0         0 $self -> {escaped_filename} =~ s/%5B/[/g;
146 0         0 $self -> {escaped_filename} =~ s/%5D/]/g;
147 0         0 $self -> {escaped_filename} =~ s/%40/\@/g;
148 0         0 $self -> {escaped_filename} =~ s/%24/\$/g;
149             # hashes. step 1
150 0         0 my @aich_tree;
151             {
152 0         0 my $base_blocks = sprintf("%d", $self -> {size} / CHUNK_SIZE);
153 0 0       0 -- $base_blocks if $self -> {size} == $base_blocks * CHUNK_SIZE;
154 0         0 &_define_base_trees_orientation( 'l', \ @aich_tree, 0, $base_blocks );
155             }
156              
157             {
158 0 0       0 open my $f, '<', $filename_to_access
  0         0  
  0         0  
159             or die sprintf( 'cannot open %s for reading: %s',
160             $file,
161             $!,
162             );
163              
164 0         0 binmode $f;
165 0         0 my ($t, $readed_bytes);
166 0         0 my $md4 = Digest::MD4 -> new;
167 0         0 while (defined($readed_bytes = read $f, $t, CHUNK_SIZE)) {
168 0         0 $md4 -> add($t);
169 0         0 $self -> {hash} .= $md4 -> clone -> digest;
170 0         0 push @{$self -> {p}}, uc $md4 -> hexdigest;
  0         0  
171 0 0       0 if ($readed_bytes) {
172 0         0 my $pos = 0;
173 0         0 my @t_sha1;
174 0         0 while ($pos < $readed_bytes) {
175 0         0 push @t_sha1, Digest::SHA::sha1( substr( $t, $pos, BLOCK_SIZE ) );
176 0         0 $pos += BLOCK_SIZE;
177             }
178             # sha1 for chunk
179 0         0 &_get_root_hash( $aich_tree[ $#{ $self -> {p} } ],
  0         0  
180             \ @t_sha1,
181             0,
182             $#t_sha1
183             );
184 0         0 $aich_tree[$#{$self -> {p}}] = $t_sha1[0];
  0         0  
185             }
186 0 0       0 last if $readed_bytes != CHUNK_SIZE;
187             }
188 0         0 close $f;
189             return undef unless defined $readed_bytes
190 0 0 0     0 && $self -> {size} == $#{$self -> {p}} * CHUNK_SIZE + $readed_bytes;
  0         0  
191             }
192              
193             # hashes. step 2
194 0 0       0 if (@{$self -> {p}} == 1) {
  0         0  
195 0         0 $self -> {hash} = $self -> {p}[0];
196             } else {
197 0         0 $self -> {hash} = uc Digest::MD4::md4_hex( $self -> {hash} );
198             }
199             # aich hashset
200 0         0 &_get_root_hash( 'l',
201             \ @aich_tree,
202             0,
203             $#aich_tree
204             );
205 0         0 $self -> {aich} = _encode_base32( $aich_tree[ 0 ] );
206 0         0 $self -> {reliable} = 1;
207              
208 0 0       0 if (ref $either) {
209 0         0 %$either = %$self;
210 0         0 1;
211             } else {
212 0         0 bless $self, $either;
213             }
214             }
215              
216             =head2 from_link
217              
218             Can be called as class or object method:
219              
220             my $tl = Ed2k_link -> from_link( 'ed2k://|file|eMule0.49c.zip|2868871|0F88EEFA9D8AD3F43DABAC9982D2450C|/' )
221             or die 'incorrect link!';
222              
223             $t1 = from_link( 'ed2k://|file|eMule0.49c-Sources.zip|5770302|195B6D8286BF184C3CC0665148D746CF|/' )
224             or die 'incorrect link!';
225              
226             Takes mandatory (filename/size/hash) and optional (AICH hash, complete hashset) fields from the link.
227             Checks some correctness of fields (acceptable symbols, length, ...).
228             If link in parameter has complete hashset, checks compliance between hash and complete hashset.
229              
230             In case of any incorrectness returns undef and object doesn't hold any link information.
231              
232             If link in parameter has AICH and/or complete hashset, sets Reliable flag to false. Otherwise it's true.
233              
234             =cut
235              
236             sub from_link {
237 1     1 1 81367 my $either = shift;
238 1 50       5 %$either = () if ref $either;
239 1         2 my $link = shift;
240 1 50       3 return undef unless defined $link;
241 1 50       11 return undef unless $link =~ m#^ed2k://\|file\|([\d\D]+?)\|(\d+)\|([\da-f]{32})\|#i;
242 1         8 my $self = { escaped_filename => $1,
243             size => $2,
244             hash => uc $3,
245             filename => Encode::decode( 'UTF-8', URI::Escape::uri_unescape( $1 ) ),
246             reliable => 1,
247             };
248              
249 1         208 $link = "|$'";
250 1 50       5 return undef unless $self -> {size};
251              
252             # complete hashset
253 1 50       3 if ($link =~ m/\|p=([\d\D]*?)\|/) {
254 0         0 my $t = uc $1;
255 0         0 $link = "|$`$'";
256 0 0       0 return undef unless $t =~ m/^([\dA-F]{32}(:[\dA-F]{32})*)$/;
257              
258 0         0 my @t = split ':', $1;
259 0         0 $t = sprintf("%d", $self -> {size} / CHUNK_SIZE);
260 0 0       0 ++ $t if $self -> {size} >= $t * CHUNK_SIZE;
261 0 0       0 return undef unless $t == @t;
262              
263 0 0       0 if (@t == 1) {
264 0 0       0 return undef unless $self -> {hash} eq $t[0];
265             } else {
266 0         0 my $t = '';
267 0         0 foreach my $bh (@t) {
268 0         0 $t .= chr(hex($&)) while $bh =~ m/../g;
269             }
270 0 0       0 return undef unless $self -> {hash} eq uc Digest::MD4::md4_hex( $t );
271 0         0 $self -> {reliable} = 0;
272             }
273 0         0 $self -> {p} = \@t;
274             }
275 1 50 33     11 $self -> {p}[0] = $self -> {hash} if $self -> {size} < CHUNK_SIZE && not exists $self -> {p};
276              
277             # aich
278 1 50       4 if ($link =~ m/\|h=([\d\D]*?)\|/) {
279 0         0 $self -> {aich} = uc $1;
280 0         0 $link = "|$`$'";
281 0 0       0 return undef unless $self -> {aich} =~ m/^[A-Z2-7]{32}$/;
282 0         0 $self -> {reliable} = 0;
283             }
284              
285 1 50       3 if (ref $either) {
286 0         0 %$either = %$self;
287 0         0 $either;
288             } else {
289 1         4 bless $self, $either;
290             }
291             }
292              
293             =head2 ok
294              
295             Instance only method. Returns true if object was successfully created and holds all required fields;
296              
297             &do_something() if $t1 -> ok;
298              
299             =cut
300              
301             sub ok {
302 1 50   1 1 551 ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
303 1   33     13 return exists $instance -> {escaped_filename} && exists $instance -> {size} && exists $instance -> {hash};
304             }
305              
306             =head2 filename
307              
308             Instance method. Returns filename as character string:
309              
310             print $t -> filename;
311              
312             =cut
313              
314             sub filename {
315 1 50   1 1 4 ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
316 1         5 $instance -> {filename};
317             }
318              
319             =head2 escaped_filename
320              
321             Instance method. Returns escaped filename (as in link);
322              
323             print $t -> escaped_filename;
324              
325             =cut
326              
327             sub escaped_filename {
328 1 50   1 1 4 ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
329 1         4 $instance -> {escaped_filename};
330             }
331              
332             =head2 filesize
333              
334             Instance method. Returns filesize;
335              
336             =cut
337              
338             sub filesize {
339 1 50   1 1 5 ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
340 1         4 $instance -> {size};
341             }
342              
343             =head2 hash
344              
345             Instance method. Returns hash field from link;
346              
347             =cut
348              
349             sub hash {
350 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
351 0           $instance -> {hash};
352             }
353              
354             =head2 has_complete_hashset
355              
356             Instance method. Returns true if object has complete hashset, false otherwise;
357              
358             =cut
359              
360             sub has_complete_hashset {
361 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
362 0 0         exists $instance -> {p} && @{$instance -> {p}};
  0            
363             }
364              
365             =head2 complete_hashset
366              
367             Instance method. Returns complete hashset if object has it. undef otherwise;
368              
369             =cut
370              
371             sub complete_hashset {
372 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
373             $instance -> has_complete_hashset ?
374 0 0         join ':', @{$instance -> {p}}
  0            
375             : undef;
376             }
377              
378             =head2 has_aich
379              
380             Instance method. Returns true if object has aich hash, false otherwise;
381              
382             =cut
383              
384             sub has_aich {
385 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
386 0           exists $instance -> {aich};
387             }
388              
389             =head2 aich
390              
391             Instance method. Returns AICH hash if object has it. undef otherwise;
392              
393             =cut
394              
395             sub aich {
396 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
397 0           $instance -> {aich};
398             }
399              
400             =head2 link
401              
402             Instance only method. Returns string representation of link. Can have parameter with options:
403              
404             h - include AICH hash if available. Recommended.
405             p - include complete hashset if available.
406              
407             my $link1 = $t -> link;
408             my $link_with_aich = $t -> link( 'h' );
409             my $link_with_hashset = $t -> link( 'p' );
410             my $iron_link = $t -> link( 'hp' );
411              
412             =cut
413              
414             sub link {
415 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
416 0           my $optional = shift;
417 0 0         return undef unless $instance -> ok;
418              
419 0           my @part = ( 'ed2k://|file',
420             $instance -> escaped_filename,
421             $instance -> filesize,
422             $instance -> hash,
423             );
424              
425 0 0         if ( defined $optional ) {
426             # complete hashset
427 0 0 0       push @part,
      0        
428             'p=' . $instance -> complete_hashset
429             if index( $optional, 'p' ) != -1
430             && $instance -> filesize >= CHUNK_SIZE
431             && $instance -> has_complete_hashset;
432              
433             # aich hashset
434 0 0 0       push @part,
435             'h=' . $instance -> aich
436             if index( $optional, 'h' ) != -1
437             && $instance -> has_aich;
438             }
439              
440 0           join '|', @part, '/';
441             }
442              
443             =head2 is_reliable
444              
445             Instance method. Returns true if object is reliable, false otherwise;
446              
447             =cut
448              
449             sub is_reliable {
450 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
451 0           $instance -> {reliable};
452             }
453              
454             =head2 set_reliable
455              
456             Instance method. Sets Reliable flag for object. Use it very carefully, or you could end up with fake link
457             that doesn't reference any file and you won't be able to download anything with them.
458              
459             Carefully means: you got string link from someone, who you trust. Or you previously created it from file
460             by yourself and saved somethere and now you're reading those links from file of database.
461             Such usage of this method is appropriated;
462              
463             =cut
464              
465             sub set_reliable {
466 0 0   0 1   ref(my $instance = shift) or Carp::croak "class usage! need to be instance usage";
467 0           $instance -> {reliable} = 1;
468             }
469              
470             =head2 equal
471              
472             Class only method.
473             Compares two Ed2k_link objects by complex rules. Returns true if they point to the same file.
474             Could fill some fields of one object with other's objects fields. Also can set Reliable flag.
475              
476             print "hey! they are the same!" if Ed2k_link -> equal($t1, $t2);
477              
478             =cut
479              
480             sub equal {
481 0     0 1   my $class = shift;
482 0 0         return undef unless @_ == 2;
483 0           my $one = shift;
484 0           my $two = shift;
485 0   0       my $res = $one -> ok && $two -> ok && $one -> filesize == $two -> filesize && $one -> hash eq $two -> hash;
486 0 0         return undef unless $res;
487 0 0 0       $res = $one -> complete_hashset eq $two -> complete_hashset
488             if $one -> has_complete_hashset && $two -> has_complete_hashset;
489 0 0         return undef unless $res;
490 0 0 0       $res = $one -> aich eq $two -> aich
491             if $one -> has_aich && $two -> has_aich;
492 0 0         return undef unless $res;
493              
494             # cases with copying complete hash or aich and setting reliable flag
495 0 0 0       if ($one -> is_reliable && $two -> is_reliable) {
    0          
    0          
496 0 0 0       if ($one -> has_complete_hashset && !$two -> has_complete_hashset) {
    0 0        
497 0           $two -> {p} = $one -> {p};
498             } elsif (!$one -> has_complete_hashset && $two -> has_complete_hashset) {
499 0           $one -> {p} = $two -> {p};
500             }
501 0 0 0       if ($one -> has_aich && !$two -> has_aich) {
    0 0        
502 0           $two -> {aich} = $one -> {aich};
503             } elsif (!$one -> has_aich && $two -> has_aich) {
504 0           $one -> {aich} = $two -> {aich};
505             }
506             } elsif ($one -> is_reliable) {
507 0           my $t = 0;
508 0 0         if ($one -> has_complete_hashset) {
509 0           ++ $t;
510 0           $two -> {p} = $one -> {p};
511             }
512 0 0         if ($one -> has_aich) {
513 0           ++ $t;
514 0           $two -> {aich} = $one -> {aich};
515             }
516 0 0         -- $t if $two -> has_complete_hashset;
517 0 0         -- $t if $two -> has_aich;
518 0 0         $two -> set_reliable if $t >= 0;
519             } elsif ($two -> is_reliable) {
520 0           my $t = 0;
521 0 0         if ($two -> has_complete_hashset) {
522 0           ++ $t;
523 0           $one -> {p} = $two -> {p};
524             }
525 0 0         if ($two -> has_aich) {
526 0           ++ $t;
527 0           $one -> {aich} = $two -> {aich};
528             }
529 0 0         -- $t if $one -> has_complete_hashset;
530 0 0         -- $t if $one -> has_aich;
531 0 0         $one -> set_reliable if $t >= 0;
532             }
533              
534 0           $res;
535             }
536              
537             1;
538             __END__