File Coverage

blib/lib/Ed2k_link.pm
Criterion Covered Total %
statement 53 210 25.2
branch 12 144 8.3
condition 2 51 3.9
subroutine 14 27 51.8
pod 15 18 83.3
total 96 450 21.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package Ed2k_link;
3             our $VERSION = '20090428';
4            
5 2     2   103941 use strict;
  2         7  
  2         551  
6 2     2   14 use warnings;
  2         5  
  2         161  
7 2     2   13 use base qw(Exporter);
  2         8  
  2         1814  
8             our @EXPORT = ();
9             our @EXPORT_OK = ();
10            
11 2     2   18 use Carp qw(croak);
  2         4  
  2         163  
12 2     2   11 use File::Basename qw();
  2         5  
  2         45  
13 2     2   8808 use URI::Escape;
  2         4240  
  2         164  
14 2     2   2568 use Digest::MD4 qw(md4_hex);
  2         6586  
  2         882  
15 2     2   3642 use Digest::SHA1 qw(sha1);
  2         7034  
  2         200  
16            
17             use constant {
18 2         6136 CHANK_SIZE => 9_728_000,
19             BLOCK_SIZE => 184_320,
20 2     2   21 };
  2         6  
21            
22             sub encode_base32 {
23 0     0 0 0 my %bits_to_char = qw# 00000 A 00001 B 00010 C 00011 D 00100 E 00101 F 00110 G 00111 H
24             01000 I 01001 J 01010 K 01011 L 01100 M 01101 N 01110 O 01111 P
25             10000 Q 10001 R 10010 S 10011 T 10100 U 10101 V 10110 W 10111 X
26             11000 Y 11001 Z 11010 2 11011 3 11100 4 11101 5 11110 6 11111 7
27             #;
28 0         0 my ($source, $bits, $res) = shift;
29 0         0 $bits .= unpack('B*', substr($source, $_, 1)) for 0 .. length($source) - 1;
30             # generally $bits could be not 40 * k length and have to be padding. but not in our case
31 0         0 $res .= $bits_to_char{$&} while $bits =~ m/.{5}/g;
32 0         0 $res;
33             }
34            
35             sub define_base_trees_orientation { # l/r, array_ref, start_idx, end_idx
36 0 0   0 0 0 if ($_[2] - $_[3] >= 0) {
    0          
37 0         0 $_[1][$_[2]] = $_[0];
38             } elsif ($_[2] + 1 == $_[3]) {
39 0         0 $_[1][$_[2]] = 'l';
40 0         0 $_[1][$_[3]] = 'r';
41             } else {
42 0         0 my $med = sprintf("%d", ($_[2] + $_[3]) / 2);
43 0 0 0     0 $med-- if $_[0] eq 'r' && $_[2] + $_[3] == $med * 2;
44 0         0 &define_base_trees_orientation('l', $_[1], $_[2], $med++);
45 0         0 &define_base_trees_orientation('r', $_[1], $med, $_[3]);
46             }
47             }
48            
49             sub get_root_hash { # l/r, array_ref, start_idx, end_idx
50 0     0 0 0 my $med = $_[3];
51 0 0       0 if ($_[2] - $_[3] >= 0) {
    0          
52 0         0 return;
53             } elsif ($_[3] - $_[2] > 1) {
54 0         0 $med = sprintf("%d", ($_[2] + $_[3]) / 2);
55 0 0 0     0 $med-- if $_[0] eq 'r' && $_[2] + $_[3] == $med * 2;
56 0         0 &get_root_hash('l', $_[1], $_[2], $med++);
57 0         0 &get_root_hash('r', $_[1], $med, $_[3]);
58             }
59 0         0 $_[1] -> [$_[2]] = sha1($_[1] -> [$_[2]], $_[1] -> [$med]);
60             }
61            
62             sub from_file {
63 0     0 1 0 my $either = shift;
64 0 0       0 %$either = () if ref $either;
65 0         0 my $file = shift;
66 0 0       0 return undef unless defined $file;
67            
68             # file must exist and be not empty!
69 0 0 0     0 return undef unless -f $file && -s $file;
70            
71 0         0 my $self = { path_to_file => $file };
72 0         0 $self -> {filename} = File::Basename::fileparse($file);
73             # emule doesn't escape #[]@$&+,;=
74 0         0 $self -> {escaped_filename} = uri_escape_utf8($self -> {filename}, '^A-Za-z0-9\-_.!~*\'()#&+,;=');
75             # []@$
76 0         0 $self -> {escaped_filename} =~ s/%5B/[/g;
77 0         0 $self -> {escaped_filename} =~ s/%5D/]/g;
78 0         0 $self -> {escaped_filename} =~ s/%40/\@/g;
79 0         0 $self -> {escaped_filename} =~ s/%24/\$/g;
80 0         0 $self -> {size} = -s $file;
81             # hashes. step 1
82 0         0 my @aich_tree;
83             {
84 0         0 my $base_blocks = sprintf("%d", $self -> {size} / CHANK_SIZE);
  0         0  
85 0 0       0 $base_blocks-- if $self -> {size} == $base_blocks * CHANK_SIZE;
86 0         0 &define_base_trees_orientation('l', \@aich_tree, 0, $base_blocks);
87             }
88            
89             {
90 0 0       0 open my $f, '<', $file or return undef;
  0         0  
91 0         0 binmode $f;
92 0         0 my ($t, $readed_bytes);
93 0         0 my $md4 = Digest::MD4 -> new;
94 0         0 while (defined($readed_bytes = read $f, $t, CHANK_SIZE)) {
95 0         0 $md4 -> add($t);
96 0         0 $self -> {hash} .= $md4 -> clone -> digest;
97 0         0 push @{$self -> {p}}, uc $md4 -> hexdigest;
  0         0  
98 0 0       0 if ($readed_bytes) {
99 0         0 my $pos = 0;
100 0         0 my @t_sha1;
101 0         0 while ($pos < $readed_bytes) {
102 0         0 push @t_sha1, sha1(substr($t, $pos, BLOCK_SIZE));
103 0         0 $pos += BLOCK_SIZE;
104             }
105             # sha1 for chank
106 0         0 &get_root_hash($aich_tree[$#{$self -> {p}}], \@t_sha1, 0, $#t_sha1);
  0         0  
107 0         0 $aich_tree[$#{$self -> {p}}] = $t_sha1[0];
  0         0  
108             }
109 0 0       0 last if $readed_bytes != CHANK_SIZE;
110             }
111 0         0 close $f;
112 0         0 return undef unless defined $readed_bytes
113 0 0 0     0 && $self -> {size} == $#{$self -> {p}} * CHANK_SIZE + $readed_bytes;
114             }
115            
116             # hashes. step 2
117 0 0       0 if (@{$self -> {p}} == 1) {
  0         0  
118 0         0 $self -> {hash} = $self -> {p}[0];
119             } else {
120 0         0 $self -> {hash} = uc md4_hex($self -> {hash});
121             }
122             # aich hashset
123 0         0 &get_root_hash('l', \@aich_tree, 0, $#aich_tree);
124 0         0 $self -> {aich} = encode_base32($aich_tree[0]);
125 0         0 $self -> {reliable} = 1;
126            
127 0 0       0 if (ref $either) {
128 0         0 %$either = %$self;
129 0         0 1;
130             } else {
131 0         0 bless $self, $either;
132             }
133             }
134            
135             sub from_link {
136 1     1 1 12 my $either = shift;
137 1 50       5 %$either = () if ref $either;
138 1         3 my $link = shift;
139 1 50       4 return undef unless defined $link;
140 1 50       14 return undef unless $link =~ m#^ed2k://\|file\|([\d\D]+?)\|(\d+)\|([\da-f]{32})\|#i;
141 1         2 my $self;
142 1         5 $self -> {escaped_filename} = $1;
143 1         6 $self -> {filename} = uri_unescape($1);
144 1         14 $self -> {size} = $2;
145 1         5 $self -> {hash} = uc $3;
146 1         4 $self -> {reliable} = 1;
147 1         4 $link = "|$'";
148 1 50       4 return undef unless $self -> {size};
149            
150             # complete hashset
151 1 50       4 if ($link =~ m/\|p=([\d\D]*?)\|/) {
152 0         0 my $t = uc $1;
153 0         0 $link = "|$`$'";
154 0 0       0 return undef unless $t =~ m/^([\dA-F]{32}(:[\dA-F]{32})*)$/;
155            
156 0         0 my @t = split ':', $1;
157 0         0 $t = sprintf("%d", $self -> {size} / CHANK_SIZE);
158 0 0       0 $t++ if $self -> {size} >= $t * CHANK_SIZE;
159 0 0       0 return undef unless $t == @t;
160            
161 0 0       0 if (@t == 1) {
162 0 0       0 return undef unless $self -> {hash} eq $t[0];
163             } else {
164 0         0 my $t = '';
165 0         0 foreach my $bh (@t) {
166 0         0 $t .= chr(hex($&)) while $bh =~ m/../g;
167             }
168 0 0       0 return undef unless $self -> {hash} eq uc md4_hex($t);
169 0         0 $self -> {reliable} = 0;
170             }
171 0         0 $self -> {p} = \@t;
172             }
173 1 50 33     19 $self -> {p}[0] = $self -> {hash} if $self -> {size} < CHANK_SIZE && not exists $self -> {p};
174            
175             # aich
176 1 50       4 if ($link =~ m/\|h=([\d\D]*?)\|/) {
177 0         0 $self -> {aich} = uc $1;
178 0         0 $link = "|$`$'";
179 0 0       0 return undef unless $self -> {aich} =~ m/^[A-Z2-7]{32}$/;
180 0         0 $self -> {reliable} = 0;
181             }
182            
183 1 50       4 if (ref $either) {
184 0         0 %$either = %$self;
185 0         0 1;
186             } else {
187 1         6 bless $self, $either;
188             }
189             }
190            
191             sub ok {
192 1 50   1 1 1268 ref(my $instance = shift) or croak "class usage! need to be instance usage";
193 1   33     21 return exists $instance -> {escaped_filename} && exists $instance -> {size} && exists $instance -> {hash};
194             }
195            
196             sub filename {
197 1 50   1 1 5 ref(my $instance = shift) or croak "class usage! need to be instance usage";
198 1         6 $instance -> {filename};
199             }
200            
201             sub escaped_filename {
202 1 50   1 1 7 ref(my $instance = shift) or croak "class usage! need to be instance usage";
203 1         6 $instance -> {escaped_filename};
204             }
205            
206             sub filesize {
207 1 50   1 1 27 ref(my $instance = shift) or croak "class usage! need to be instance usage";
208 1         7 $instance -> {size};
209             }
210            
211             sub hash {
212 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
213 0           $instance -> {hash};
214             }
215            
216             sub has_complete_hashset {
217 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
218 0 0         exists $instance -> {p} && @{$instance -> {p}};
  0            
219             }
220            
221             sub complete_hashset {
222 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
223 0           $instance -> has_complete_hashset ?
224 0 0         join ':', @{$instance -> {p}}
225             : undef;
226             }
227            
228             sub has_aich {
229 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
230 0           exists $instance -> {aich};
231             }
232            
233             sub aich {
234 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
235 0           $instance -> {aich};
236             }
237            
238             sub link {
239 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
240 0           my $optional = shift;
241 0 0         return undef unless $instance -> ok;
242 0           my $res = 'ed2k://|file|'.$instance -> escaped_filename.'|'.$instance -> filesize.'|'.$instance -> hash.'|';
243 0 0         if (defined $optional) {
244             # complete hashset
245 0 0 0       $res .= "p=" . $instance -> complete_hashset . '|'
      0        
246             if $optional =~ /p/ && $instance -> filesize >= CHANK_SIZE && $instance -> has_complete_hashset;
247             # aich hashset
248 0 0 0       $res .= 'h=' . $instance -> aich . '|' if $optional =~ /h/ && $instance -> has_aich;
249             }
250 0           $res .= '/';
251             }
252            
253             sub is_reliable {
254 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
255 0           $instance -> {reliable};
256             }
257            
258             sub set_reliable {
259 0 0   0 1   ref(my $instance = shift) or croak "class usage! need to be instance usage";
260 0           $instance -> {reliable} = 1;
261             }
262            
263             sub equal {
264 0     0 1   my $class = shift;
265 0 0         return undef unless @_ == 2;
266 0           my $one = shift;
267 0           my $two = shift;
268 0   0       my $res = $one -> ok && $two -> ok && $one -> filesize == $two -> filesize && $one -> hash eq $two -> hash;
269 0 0         return undef unless $res;
270 0 0 0       $res = $one -> complete_hashset eq $two -> complete_hashset
271             if $one -> has_complete_hashset && $two -> has_complete_hashset;
272 0 0         return undef unless $res;
273 0 0 0       $res = $one -> aich eq $two -> aich
274             if $one -> has_aich && $two -> has_aich;
275 0 0         return undef unless $res;
276            
277             # cases with copying complete hash or aich and setting reliable flag
278 0 0 0       if ($one -> is_reliable && $two -> is_reliable) {
    0          
    0          
279 0 0 0       if ($one -> has_complete_hashset && !$two -> has_complete_hashset) {
    0 0        
280 0           $two -> {p} = $one -> {p};
281             } elsif (!$one -> has_complete_hashset && $two -> has_complete_hashset) {
282 0           $one -> {p} = $two -> {p};
283             }
284 0 0 0       if ($one -> has_aich && !$two -> has_aich) {
    0 0        
285 0           $two -> {aich} = $one -> {aich};
286             } elsif (!$one -> has_aich && $two -> has_aich) {
287 0           $one -> {aich} = $two -> {aich};
288             }
289             } elsif ($one -> is_reliable) {
290 0           my $t = 0;
291 0 0         if ($one -> has_complete_hashset) {
292 0           $t++;
293 0           $two -> {p} = $one -> {p};
294             }
295 0 0         if ($one -> has_aich) {
296 0           $t++;
297 0           $two -> {aich} = $one -> {aich};
298             }
299 0 0         $t-- if $two -> has_complete_hashset;
300 0 0         $t-- if $two -> has_aich;
301 0 0         $two -> set_reliable if $t >= 0;
302             } elsif ($two -> is_reliable) {
303 0           my $t = 0;
304 0 0         if ($two -> has_complete_hashset) {
305 0           $t++;
306 0           $one -> {p} = $two -> {p};
307             }
308 0 0         if ($two -> has_aich) {
309 0           $t++;
310 0           $one -> {aich} = $two -> {aich};
311             }
312 0 0         $t-- if $one -> has_complete_hashset;
313 0 0         $t-- if $one -> has_aich;
314 0 0         $one -> set_reliable if $t >= 0;
315             }
316            
317 0           $res;
318             }
319            
320             1;
321             __END__