File Coverage

blib/lib/Protocol/BitTorrent/Metainfo.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Protocol::BitTorrent::Metainfo;
2             {
3             $Protocol::BitTorrent::Metainfo::VERSION = '0.004';
4             }
5 1     1   21145 use strict;
  1         2  
  1         35  
6 1     1   4 use warnings FATAL => 'all', NONFATAL => 'redefine';
  1         1  
  1         42  
7 1     1   855 use POSIX qw(floor ceil strftime);
  1         6961  
  1         8  
8 1     1   1080 use List::Util qw(sum);
  1         2  
  1         97  
9 1     1   808 use Try::Tiny;
  1         1489  
  1         48  
10 1     1   890 use URI;
  1         8952  
  1         44  
11 1     1   906 use URI::QueryParam;
  1         776  
  1         28  
12 1     1   20365 use Digest::SHA qw(sha1);
  1         8321  
  1         130  
13 1     1   2123 use Convert::Bencode_XS qw();
  0            
  0            
14             use parent qw(Protocol::BitTorrent::Bencode);
15              
16             =head1 NAME
17              
18             Protocol::BitTorrent::Metainfo - support for metainfo as found in .torrent files
19              
20             =head1 VERSION
21              
22             version 0.004
23              
24             =head1 SYNOPSIS
25              
26             use Protocol::BitTorrent::Metainfo;
27             print Protocol::BitTorrent::Metainfo->new->parse_info(...)->announce_url;
28              
29             =head1 DESCRIPTION
30              
31             See L for top-level documentation.
32              
33             =cut
34              
35             use constant PEER_ID_LENGTH => 20;
36              
37             =head1 METHODS
38              
39             =cut
40              
41             =head2 new
42              
43             Instantiate a new metainfo object.
44              
45             Takes the following named parameters:
46              
47             =over 4
48              
49             =item * announce - tracker URL for announcing peers
50              
51             =item * comment - optional comment for this torrent
52              
53             =item * encoding - encoding for the torrent, typically UTF8
54              
55             =back
56              
57             =cut
58              
59             sub new {
60             my $self = bless { }, shift;
61             my %args = @_;
62              
63             $self->{$_} = delete $args{$_} for qw(announce comment encoding created);
64             $self->{files} ||= [ ];
65             die "Unknown metainfo parameter: $_\n" for sort keys %args;
66             return $self;
67             }
68              
69             =head2 parse_info
70              
71             Parse the given metainfo structure to populate a new object. Used when
72             reading an existing torrent file:
73              
74             my $data = File::Slurp::read_file($filename, { binmode => ':raw' });
75             $data = Protocol::BitTorrent::Metainfo->bdecode($data);
76             my $torrent = Protocol::BitTorrent::Metainfo->new->parse_info($data);
77              
78             =cut
79              
80             sub parse_info {
81             my $self = shift;
82             my $info = shift;
83              
84             $self->$_($info->{$_}) for grep exists $info->{$_}, qw(announce comment encoding);
85             $self->{created} = $info->{'creation date'} if exists $info->{'creation date'};
86             if(exists $info->{info}) {
87             my @files;
88             if($info->{info}->{files}) {
89             $self->{root_path} = $info->{info}{name};
90             foreach my $f (@{$info->{info}{files}}) {
91             push @files, {
92             length => $f->{length},
93             name => join '/', @{$f->{path}},
94             };
95             }
96             } else {
97             push @files, +{
98             map { $_ => $info->{info}->{$_} } qw(name length)
99             };
100             }
101             $self->{files} = \@files;
102             $self->{piece_length} = $info->{info}->{'piece length'};
103             $self->{pieces} = $info->{info}->{pieces};
104             $self->{is_private} = $info->{info}->{private} if exists $info->{info}->{private};
105             }
106             return $self;
107             }
108              
109             sub root_path {
110             my $self = shift;
111             if(@_) {
112             $self->{root_path} = shift;
113             return $self
114             }
115             return $self->{root_path};
116             }
117              
118             =head2 infohash
119              
120             Returns the infohash for this torrent. Defined as the 20-character SHA1
121             hash of the info data.
122              
123             =cut
124              
125             sub infohash {
126             my $self = shift;
127             return sha1(
128             try {
129             $self->bencode($self->file_info)
130             } catch {
131             require Data::Dumper;
132             die "Invalid infohash data: $_ from " . Data::Dumper::Dumper($self->file_info) . "\n"
133             }
134             );
135             }
136              
137             =head2 file_info
138              
139             Returns or updates the info data (referred to as an 'info dictionary' in the spec).
140              
141             =cut
142              
143             sub file_info {
144             my $self = shift;
145             unless(exists $self->{info}) {
146             $self->{info} = {
147             'piece length' => $self->piece_length,
148             'pieces' => $self->pieces,
149             };
150             $self->{info}->{private} = $self->is_private if $self->has_private_flag;
151             if($self->files == 1) {
152             my ($file) = $self->files;
153             $self->{info}{name} = $file->{name};
154             $self->{info}{length} = $file->{length};
155             } else {
156             $self->{info}{name} = $self->root_path;
157             $self->{info}{files} = [];
158             foreach my $file ($self->files) {
159             push @{ $self->{info}{files} }, {
160             'length' => $file->{length},
161             'path' => [ split m{/}, $file->{name} ],
162             }
163             }
164             }
165             }
166             return $self->{info};
167             }
168              
169             =head2 peer_id
170              
171             Returns the current peer ID. This is a 20-character string used to
172             differentiate peers connecting to a torrent.
173              
174             Will generate a new peer ID if one has not already been assigned.
175              
176             =cut
177              
178             sub peer_id {
179             my $self = shift;
180             if(@_) {
181             $self->{peer_id} = shift;
182             }
183             $self->{peer_id} = $self->generate_peer_id unless exists $self->{peer_id};
184             return $self->{peer_id};
185             }
186              
187             =head2 generate_peer_id_azureus
188              
189             Generate a new peer ID using the Azureus style:
190              
191             -BT0001-980123456789
192              
193             Takes the following parameters:
194              
195             =over 4
196              
197             =item * $type - the 2-character type, defaults to PB (for "L").
198              
199             =item * $version - the 4-character version code, should be numeric although this is not
200             enforced. Defaults to current package version with . characters stripped.
201              
202             =item * $suffix - trailing string data to append to the peer ID, defaults to random
203             decimal digits.
204              
205             =back
206              
207             Example invocation:
208              
209             $torrent->generate_peer_id_azureus('XX', '0100', '0123148')
210              
211             =cut
212              
213             sub generate_peer_id_azureus {
214             my $self = shift;
215             my $type = shift || 'PB';
216             my $version = shift;
217             my $suffix = shift || '';
218              
219             (($version = $self->VERSION || '') =~ tr/\.//d) unless defined $version;
220             $version = "0$version" while length $version < 4;
221             my $peer_id = '-' . $type . $version . '-' . $suffix;
222             $peer_id .= floor(rand(10)) while length $peer_id < PEER_ID_LENGTH;
223             $peer_id = substr $peer_id, 0, PEER_ID_LENGTH if length $peer_id > PEER_ID_LENGTH;
224             return $peer_id;
225             }
226              
227             =head2 generate_peer_id
228              
229             Generates a peer ID using the default method (currently Azureus which is the only
230             defined method, see L).
231              
232             =cut
233              
234             sub generate_peer_id { shift->generate_peer_id_azureus(@_) }
235              
236             =head2 files
237              
238             Returns a list of the files in this torrent, or replaces the current list if given
239             an arrayref.
240              
241             =cut
242              
243             sub files {
244             my $self = shift;
245             if(@_) {
246             $self->{files} = shift;
247             return $self;
248             }
249             return map +{ %$_ }, @{$self->{files}};
250             }
251              
252             =head2 announce
253              
254             Get/set tracker announce URL.
255              
256             =cut
257              
258             sub announce {
259             my $self = shift;
260             if(@_) {
261             $self->{announce} = shift;
262             return $self;
263             }
264             return $self->{announce};
265             }
266              
267             =head2 piece_length
268              
269             Get/set current piece length. Recommended values seem to be between 256KB and 1MB.
270              
271             =cut
272              
273             sub piece_length {
274             my $self = shift;
275             if(@_) {
276             $self->{piece_length} = shift;
277             return $self;
278             }
279             return $self->{piece_length};
280             }
281              
282             =head2 total_length
283              
284             Returns the total length for all files in this torrent.
285              
286             =cut
287              
288             sub total_length {
289             my $self = shift;
290             return sum(map $_->{length}, $self->files) || 0;
291             }
292              
293             =head2 total_pieces
294              
295             Returns the total number of pieces in this torrent, equivalent to
296             the total length of all files divided by the piece size (and rounded
297             up to include the last partial piece as required).
298              
299             =cut
300              
301             sub total_pieces {
302             my $self = shift;
303             return ceil($self->total_length / $self->piece_length);
304             }
305              
306             =head2 pieces
307              
308             Returns the combined hash string representing the pieces in this torrent.
309             Will be a byte string of length L * 20.
310              
311             =cut
312              
313             sub pieces { shift->{pieces} }
314              
315             =head2 is_private
316              
317             Returns 1 if this is a private torrent, 0 otherwise.
318              
319             =cut
320              
321             sub is_private { shift->{is_private} ? 1 : 0 }
322              
323             =head2 has_private_flag
324              
325             Returns true if this torrent has the optional C< private > flag.
326              
327             =cut
328              
329             sub has_private_flag { exists shift->{is_private} ? 1 : 0 }
330              
331             =head2 encoding
332              
333             Get/set current encoding for metainfo strings.
334              
335             =cut
336              
337             sub encoding {
338             my $self = shift;
339             if(@_) {
340             $self->{encoding} = shift;
341             return $self;
342             }
343             return $self->{encoding};
344             }
345              
346             =head2 trackers
347              
348             Get/set trackers. Takes an arrayref when setting, returns a list.
349              
350             =cut
351              
352             sub trackers {
353             my $self = shift;
354             if(@_) {
355             $self->{trackers} = shift;
356             return $self;
357             }
358             return @{ $self->{trackers} };
359             }
360              
361             =head2 comment
362              
363             Get/set metainfo comment.
364              
365             =cut
366              
367             sub comment {
368             my $self = shift;
369             if(@_) {
370             $self->{comment} = shift;
371             return $self;
372             }
373             return $self->{comment};
374             }
375              
376             =head2 created
377              
378             Get/set creation time of this torrent, as epoch value (seconds since 1st Jan 1970).
379              
380             =cut
381              
382             sub created {
383             my $self = shift;
384             if(@_) {
385             $self->{created} = shift;
386             return $self;
387             }
388             return $self->{created};
389             }
390              
391             =head2 created_iso8601
392              
393             Returns the L value as a string in ISO8601 format.
394              
395             Example:
396              
397             2011-04-01T18:04:00
398              
399             =cut
400              
401             sub created_iso8601 {
402             my $self = shift;
403             my $ts = $self->created;
404             return undef unless defined $ts;
405             return strftime("%Y-%m-%dT%H:%M:%S", gmtime($ts));
406             }
407              
408             =head2 created_by
409              
410             Get/set 'created by' field, indicating who created this torrent.
411              
412             =cut
413              
414             sub created_by {
415             my $self = shift;
416             if(@_) {
417             $self->{created_by} = shift;
418             return $self;
419             }
420             return $self->{created_by};
421             }
422              
423             =head2 as_metainfo
424              
425             Returns the object formatted as a metainfo hashref, suitable for
426             bencoding into a .torrent file.
427              
428             =cut
429              
430             sub as_metainfo {
431             my $self = shift;
432             my %info = (
433             announce => $self->announce,
434             );
435             $info{'creation date'} = $self->created if defined $self->created;
436             $info{'comment'} = $self->comment if defined $self->comment;
437             $info{'created by'} = $self->created_by if defined $self->created_by;
438             die "Undef value for $_" for sort grep !defined($info{$_}), keys %info;
439              
440             $info{'created by'} = $self->created_by if defined $self->created_by;
441             ($info{info}) = $self->files;
442             $info{info}{pieces} = $self->pieces;
443             return \%info;
444             }
445              
446             =head2 add_file
447              
448             Adds the given file to this torrent. If the torrent already has a file
449             and is in single mode, will switch to multi mode.
450              
451             =cut
452              
453             sub add_file {
454             my $self = shift;
455             my $filename = shift;
456             my $size = -s $filename;
457             # $self->piece_length(262144);
458             $self->piece_length(1048576);
459             my $hash = $self->hash_for_file($filename);
460              
461             push @{$self->{files}}, {
462             'name' => $filename,
463             'length' => $size,
464             'piece length' => $self->piece_length,
465             };
466             $self->{'pieces'} = $hash;
467             return $self;
468             }
469              
470             =head2 hash_for_file
471              
472             Returns the SHA1 hash for the pieces in the given file.
473              
474             =cut
475              
476             sub hash_for_file {
477             my $self = shift;
478             my $filename = shift;
479              
480             my @piece_hash;
481             open my $fh, '<', $filename or die "Failed to open $filename - $!\n";
482             my $piece_length = $self->piece_length;
483             while($fh->read(my $buf, $piece_length)) {
484             push @piece_hash, sha1($buf) if defined $buf && length $buf;
485             }
486             $fh->close or die $!;
487             join '', @piece_hash;
488             }
489              
490             =head2 announce_url
491              
492             Returns the tracker announce URL
493              
494             Takes the following named parameters:
495              
496             =over 4
497              
498             =item * uploaded - number of bytes uploaded so far by this client
499              
500             =item * downloaded - number of bytes downloaded so far by this client
501              
502             =item * left - number of bytes left for this client to transfer
503              
504             =item * port - (optional) the port this client is listening on, defaults to 6881
505              
506             =item * event - (optional) type of event, can be started, stopped or completed. If
507             not supplied, this will be treated as an update of a running torrent.
508              
509             =back
510              
511             =cut
512              
513             sub announce_url {
514             my $self = shift;
515             my %args = @_;
516              
517             my $uri = URI->new($self->announce);
518             $uri->query_param(info_hash => $self->infohash);
519             $uri->query_param(peer_id => $self->peer_id);
520             $uri->query_param(port => $args{port} || 6881);
521             $uri->query_param(uploaded => $args{uploaded} || 0);
522             $uri->query_param(downloaded => $args{downloaded} || 0);
523             $uri->query_param(left => $args{left} || 0);
524             $uri->query_param(event => $args{event}) if exists $args{event};
525             return $uri->as_string;
526             }
527              
528             =head2 scrape_url
529              
530             Returns the scrape URL, if there is one. Scrape URLs are only defined if the L
531             contains C< /announce > with no subsequent C< / > characters. Returns undef if a scrape URL
532             cannot be generated.
533              
534             =cut
535              
536             sub scrape_url {
537             my $self = shift;
538             unless(exists $self->{scrape_url}) {
539             my $scrape_url;
540             my $uri = URI->new($self->announce);
541             if((my $path = $uri->path) =~ s{/announce([^/]*)$}{/scrape$1}) {
542             $uri->path($path);
543             $uri->query_param(info_hash => $self->infohash);
544             $scrape_url = $uri->as_string;
545             }
546             $self->{scrape_url} = $scrape_url;
547             }
548             return $self->{scrape_url};
549             }
550              
551             sub VERSION { require Protocol::BitTorrent; $Protocol::BitTorrent::VERSION }
552              
553             1;
554              
555             __END__