File Coverage

blib/lib/Net/BitTorrent/Torrent/Generator.pm
Criterion Covered Total %
statement 23 23 100.0
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 31 31 100.0


line stmt bran cond sub pod time code
1 1     1   234268 use v5.40;
  1         5  
2 1     1   9 use feature 'class';
  1         1  
  1         183  
3 1     1   9 no warnings 'experimental::class';
  1         2  
  1         53  
4 1     1   677 use Net::BitTorrent::Emitter;
  1         4  
  1         108  
5             class Net::BitTorrent::Torrent::Generator v2.0.0 : isa(Net::BitTorrent::Emitter) {
6 1     1   704 use Net::BitTorrent::Protocol::BEP03::Bencode qw[bencode];
  1         3  
  1         101  
7 1     1   658 use Digest::SHA qw[sha1 sha256];
  1         4179  
  1         145  
8 1     1   1009 use Path::Tiny;
  1         18967  
  1         2178  
9             field $base_path : param;
10             field $piece_length : param = 262144; # 256KiB
11             field @files;
12             field @trackers;
13             field @nodes;
14             field $private = 0;
15             field $align_files = 0;
16             method set_align_files ($val) { $align_files = $val }
17             method set_private ($val) { $private = $val }
18             method add_tracker ($url) { push @trackers, $url }
19             method add_node ( $h, $p ) { push @nodes, [ $h, $p ] }
20              
21             method add_file ($rel_path) {
22             my $abs = path($base_path)->child($rel_path);
23             if ( !$abs->exists ) {
24             $self->_emit( log => "File does not exist: $abs", level => 'fatal' );
25             return;
26             }
27             if ( $align_files && @files && $files[-1]{size} % $piece_length != 0 ) {
28             my $pad = $piece_length - ( $files[-1]{size} % $piece_length );
29             push @files, { rel => ".pad/$pad", size => $pad, padding => 1 };
30             }
31             push @files, { rel => $rel_path, abs => $abs, size => $abs->stat->size };
32             }
33              
34             method generate_v1 () {
35             my $info = $self->_base_info();
36             $info->{pieces} = $self->_generate_pieces_v1();
37             return $self->_wrap_torrent($info);
38             }
39              
40             method generate_v2 () {
41             my ( $file_tree, $piece_layers ) = $self->_generate_v2_data();
42             my $info = {
43             name => path($base_path)->basename,
44             'piece length' => $piece_length,
45             'file tree' => $file_tree,
46             'meta version' => 2,
47             private => $private,
48             };
49             return $self->_wrap_torrent( $info, $piece_layers );
50             }
51              
52             method generate_hybrid () {
53             my ( $file_tree, $piece_layers ) = $self->_generate_v2_data();
54             my $info = $self->_base_info();
55             $info->{'file tree'} = $file_tree;
56             $info->{'meta version'} = 2;
57             $info->{pieces} = $self->_generate_pieces_v1();
58             return $self->_wrap_torrent( $info, $piece_layers );
59             }
60              
61             method _base_info () {
62             my $info = { name => path($base_path)->basename, 'piece length' => $piece_length, private => $private, };
63             if ( @files == 1 && !$files[0]{padding} ) {
64             $info->{length} = $files[0]{size};
65             }
66             else {
67             $info->{files} = [ map { { length => $_->{size}, path => [ split m{/}, $_->{rel} ] } } @files ];
68             }
69             return $info;
70             }
71              
72             method _wrap_torrent ( $info, $piece_layers = undef ) {
73             my $torrent = { info => $info, 'created by' => 'Net::BitTorrent 2.0.0', 'creation date' => time(), };
74             $torrent->{'piece layers'} = $piece_layers if $piece_layers;
75             $torrent->{announce} = $trackers[0] if @trackers;
76             $torrent->{'announce-list'} = [ map { [$_] } @trackers ] if @trackers > 1;
77             $torrent->{nodes} = \@nodes if @nodes;
78             return bencode($torrent);
79             }
80              
81             method _generate_pieces_v1 () {
82             my $pieces = '';
83             my $buffer = '';
84             for my $file (@files) {
85             if ( $file->{padding} ) {
86             $buffer .= "\0" x $file->{size};
87             while ( length($buffer) >= $piece_length ) {
88             $pieces .= sha1( substr( $buffer, 0, $piece_length, '' ) );
89             }
90             next;
91             }
92             my $fh = $file->{abs}->openr_raw;
93             while ( read( $fh, my $chunk, $piece_length - length($buffer) ) ) {
94             $buffer .= $chunk;
95             if ( length($buffer) == $piece_length ) {
96             $pieces .= sha1($buffer);
97             $buffer = '';
98             }
99             }
100             }
101             $pieces .= sha1($buffer) if length($buffer) > 0;
102             return $pieces;
103             }
104              
105             method _generate_v2_data () {
106 1     1   657 use Digest::Merkle::SHA256;
  1         2977  
  1         585  
107             my $file_tree = {};
108             my %piece_layers;
109             for my $file ( grep { !$_->{padding} } @files ) {
110             my $merkle = Digest::Merkle::SHA256->new( file_size => $file->{size} );
111             my $fh = $file->{abs}->openr_raw;
112             my $block_idx = 0;
113             while ( read( $fh, my $block, 16384 ) ) {
114             $merkle->set_block( $block_idx++, sha256($block) );
115             }
116             my @path = split m{/}, $file->{rel};
117             my $curr = $file_tree;
118             my $name = pop @path;
119             $curr = ( $curr->{$_} //= {} ) for @path;
120             $curr->{$name} = { '' => { length => $file->{size}, 'pieces root' => $merkle->root } };
121             if ( $file->{size} > $piece_length ) {
122             $piece_layers{ $merkle->root } = $merkle->get_piece_layer($piece_length);
123             }
124             }
125             return ( $file_tree, \%piece_layers );
126             }
127             } 1;