File Coverage

blib/lib/Image/ExifTool/Torrent.pm
Criterion Covered Total %
statement 106 131 80.9
branch 54 94 57.4
condition 9 23 39.1
subroutine 7 7 100.0
pod 0 4 0.0
total 176 259 67.9


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Torrent.pm
3             #
4             # Description: Read information from BitTorrent file
5             #
6             # Revisions: 2013/08/27 - P. Harvey Created
7             #
8             # References: 1) https://wiki.theory.org/BitTorrentSpecification
9             #------------------------------------------------------------------------------
10              
11             package Image::ExifTool::Torrent;
12              
13 1     1   4483 use strict;
  1         4  
  1         33  
14 1     1   6 use vars qw($VERSION);
  1         2  
  1         39  
15 1     1   17 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         6120  
16              
17             $VERSION = '1.06';
18              
19             sub ReadBencode($$$);
20             sub ExtractTags($$$;$$@);
21              
22             # tags extracted from BitTorrent files
23             %Image::ExifTool::Torrent::Main = (
24             GROUPS => { 2 => 'Document' },
25             NOTES => q{
26             Below are tags commonly found in BitTorrent files. As well as these tags,
27             any other existing tags will be extracted. For convenience, list items are
28             expanded into individual tags with an index in the tag name, but only the
29             tags with index "1" are listed in the tables below. See
30             L for the BitTorrent
31             specification.
32             },
33             'announce' => { },
34             'announce-list' => { Name => 'AnnounceList1' },
35             'comment' => { },
36             'created by' => { Name => 'Creator' }, # software used to create the torrent
37             'creation date' => {
38             Name => 'CreateDate',
39             Groups => { 2 => 'Time' },
40             ValueConv => 'ConvertUnixTime($val,1)',
41             PrintConv => '$self->ConvertDateTime($val)',
42             },
43             'encoding' => { },
44             'info' => {
45             SubDirectory => { TagTable => 'Image::ExifTool::Torrent::Info' },
46             Notes => 'extracted as a structure with the Struct option',
47             },
48             'url-list' => { Name => 'URLList1' },
49             );
50              
51             %Image::ExifTool::Torrent::Info = (
52             GROUPS => { 2 => 'Document' },
53             'file-duration' => { Name => 'File1Duration' },
54             'file-media' => { Name => 'File1Media' },
55             'files' => { SubDirectory => { TagTable => 'Image::ExifTool::Torrent::Files' } },
56             'length' => { },
57             'md5sum' => { Name => 'MD5Sum' },
58             'name' => { },
59             'name.utf-8' => { Name => 'NameUTF-8' },
60             'piece length' => { Name => 'PieceLength' },
61             'pieces' => {
62             Name => 'Pieces',
63             Notes => 'concatenation of 20-byte SHA-1 digests for each piece',
64             },
65             'private' => { },
66             'profiles' => { SubDirectory => { TagTable => 'Image::ExifTool::Torrent::Profiles' } },
67             );
68              
69             %Image::ExifTool::Torrent::Profiles = (
70             GROUPS => { 2 => 'Document' },
71             'width' => { Name => 'Profile1Width' },
72             'height' => { Name => 'Profile1Height' },
73             'acodec' => { Name => 'Profile1AudioCodec' },
74             'vcodec' => { Name => 'Profile1VideoCodec' },
75             );
76              
77             %Image::ExifTool::Torrent::Files = (
78             GROUPS => { 2 => 'Document' },
79             'length' => { Name => 'File1Length', PrintConv => 'ConvertFileSize($val)' },
80             'md5sum' => { Name => 'File1MD5Sum' },
81             'path' => { Name => 'File1Path', JoinPath => 1 },
82             'path.utf-8' => { Name => 'File1PathUTF-8', JoinPath => 1 },
83             );
84              
85             #------------------------------------------------------------------------------
86             # Read 64kB more data into buffer
87             # Inputs: 0) RAF ref, 1) buffer ref
88             # Returns: number of bytes read
89             # Notes: Sets BencodeEOF element of RAF on end of file
90             sub ReadMore($$)
91             {
92 1     1 0 3 my ($raf, $dataPt) = @_;
93 1         2 my $buf2;
94 1         3 my $n = $raf->Read($buf2, 65536);
95 1 50       6 $$raf{BencodeEOF} = 1 if $n != 65536;
96 1 50       9 $$dataPt = substr($$dataPt, pos($$dataPt)) . $buf2 if $n;
97 1         3 return $n;
98             }
99              
100             #------------------------------------------------------------------------------
101             # Read bencoded value
102             # Inputs: 0) ExifTool ref, 1) input file, 2) buffer (pos must be set to current position)
103             # Returns: HASH ref, ARRAY ref, SCALAR ref, SCALAR, or undef on error or end of data
104             # Notes: Sets BencodeError element of RAF on any error
105             sub ReadBencode($$$)
106             {
107 70     70 0 103 my ($et, $raf, $dataPt) = @_;
108              
109             # read more if necessary (keep a minimum of 64 bytes in the buffer)
110 70         98 my $pos = pos($$dataPt);
111 70 50       117 return undef unless defined $pos;
112 70         91 my $remaining = length($$dataPt) - $pos;
113 70 100 100     141 ReadMore($raf, $dataPt) if $remaining < 64 and not $$raf{BencodeEOF};
114              
115             # read next token
116 70 50       176 $$dataPt =~ /(.)/sg or return undef;
117              
118 70         100 my $val;
119 70         117 my $tok = $1;
120 70 100 33     300 if ($tok eq 'i') { # integer
    100          
    100          
    100          
    50          
121 6 50       31 $$dataPt =~ /\G(-?\d+)e/g or return $val;
122 6         12 $val = $1;
123             } elsif ($tok eq 'd') { # dictionary
124 6         11 $val = { };
125 6         11 for (;;) {
126 26         56 my $k = ReadBencode($et, $raf, $dataPt);
127 26 100       52 last unless defined $k;
128             # the key must be a byte string
129 20 50       38 if (ref $k) {
130 0 0       0 ref $k ne 'SCALAR' and $$raf{BencodeError} = 'Bad dictionary key', last;
131 0         0 $k = $$k;
132             }
133 20         34 my $v = ReadBencode($et, $raf, $dataPt);
134 20 50       47 last unless defined $v;
135 20         58 $$val{$k} = $v;
136             }
137             } elsif ($tok eq 'l') { # list
138 8         13 $val = [ ];
139 8         12 for (;;) {
140 23         56 my $v = ReadBencode($et, $raf, $dataPt);
141 23 100       43 last unless defined $v;
142 15         30 push @$val, $v;
143             }
144             } elsif ($tok eq 'e') { # end of dictionary or list
145             # return undef (no error)
146             } elsif ($tok =~ /^\d$/ and $$dataPt =~ /\G(\d*):/g) { # byte string
147 36         80 my $len = $tok . $1;
148 36         68 my $more = $len - (length($$dataPt) - pos($$dataPt));
149 36         44 my $value;
150 36 50       56 if ($more <= 0) {
    0          
151 36         70 $value = substr($$dataPt,pos($$dataPt),$len);
152 36         73 pos($$dataPt) = pos($$dataPt) + $len;
153             } elsif ($more > 10000000) {
154             # just skip over really long values
155 0 0       0 $val = \ "(Binary data $len bytes)" if $raf->Seek($more, 1);
156             } else {
157             # need to read more from file
158 0         0 my $buff;
159 0         0 my $n = $raf->Read($buff, $more);
160 0 0       0 if ($n == $more) {
161 0         0 $value = substr($$dataPt,pos($$dataPt)) . $buff;
162 0         0 $$dataPt = '';
163 0         0 pos($$dataPt) = 0;
164             }
165             }
166 36 50       71 if (defined $value) {
    0          
167             # return as binary data unless it is a reasonable-length ASCII string
168 36 50       82 if (length($value) > 256) {
    100          
169 0         0 $val = \$value;
170             } elsif ($value =~ /[^\t\x20-\x7e]/) {
171 1 50       11 if (Image::ExifTool::IsUTF8(\$value) >= 0) {
172 0         0 $val = $et->Decode($value, 'UTF8');
173             } else {
174 1         4 $val = \$value;
175             }
176             } else {
177 35         59 $val = $value;
178             }
179             } elsif (not defined $val) {
180 0         0 $$raf{BencodeError} = 'Truncated byte string';
181             }
182             } else {
183 0         0 $$raf{BencodeError} = 'Bad format';
184             }
185 70         128 return $val;
186             }
187              
188             #------------------------------------------------------------------------------
189             # Extract tags from dictionary hash
190             # Inputs: 0) ExifTool ref, 1) dictionary hash reference, 2) tag table ref,
191             # 3) parent hash ID, 4) parent hash name, 5-N) list indices
192             # Returns: number of tags extracted
193             sub ExtractTags($$$;$$@)
194             {
195 6     6 0 17 my ($et, $hashPtr, $tagTablePtr, $baseID, $baseName, @index) = @_;
196 6         8 my $count = 0;
197 6         11 my $tag;
198 6         40 foreach $tag (sort keys %$hashPtr) {
199 20         36 my $val = $$hashPtr{$tag};
200 20         30 my ($i, $j, @more);
201 20         38 for (; defined $val; $val = shift @more) {
202 27 50       52 my $id = defined $baseID ? "$baseID/$tag" : $tag;
203 27 50       58 unless ($$tagTablePtr{$id}) {
204 0         0 my $name = ucfirst $tag;
205             # capitalize all words in tag name and remove illegal characters
206 0         0 $name =~ s/[^-_a-zA-Z0-9]+(.?)/\U$1/g;
207 0 0 0     0 $name = "Tag$name" if length($name) < 2 or $name !~ /^[A-Z]/;
208 0 0       0 $name = $baseName . $name if defined $baseName; # add base name if necessary
209 0         0 AddTagToTable($tagTablePtr, $id, { Name => $name });
210 0         0 $et->VPrint(0, " [adding $id '${name}']\n");
211             }
212 27 50       68 my $tagInfo = $et->GetTagInfo($tagTablePtr, $id) or next;
213 27 100       65 if (ref $val eq 'ARRAY') {
214 8 100       18 if ($$tagInfo{JoinPath}) {
215 4 50       10 $val = join '/', map { ref $_ ? '(Binary data)' : $_ } @$val;
  5         23  
216             } else {
217 4         11 push @more, @$val;
218 4 100       11 next if ref $more[0] eq 'ARRAY'; # continue expanding nested lists
219 3         5 $val = shift @more;
220 3 50       10 $i or $i = 0, push(@index, $i);
221             }
222             }
223 26 100       56 $index[-1] = ++$i if defined $i;
224 26 100       50 if (@index) {
225 17         48 $id .= join '_', @index; # add instance number(s) to tag ID
226 17 50       37 unless ($$tagTablePtr{$id}) {
227 17         41 my $name = $$tagInfo{Name};
228             # embed indices at position of '1' in tag name
229 17         30 my $n = ($name =~ tr/1/#/);
230 17         39 for ($j=0; $j<$n; ++$j) {
231 13   50     34 my $idx = $index[$j] || '';
232 13         62 $name =~ s/#/$idx/;
233             }
234             # put remaining indices at end of tag name
235 17         37 for (; $j<@index; ++$j) {
236 4 50       19 $name .= '_' if $name =~ /\d$/;
237 4         11 $name .= $index[$j];
238             }
239 17         95 AddTagToTable($tagTablePtr, $id, { %$tagInfo, Name => $name });
240             }
241 17 50       46 $tagInfo = $et->GetTagInfo($tagTablePtr, $id) or next;
242             }
243 26 100       50 if (ref $val eq 'HASH') {
244 5 0 33     14 if ($et->Options('Struct') and $tagInfo and $$tagInfo{Name} eq 'Info') {
      33        
245 0         0 $et->FoundTag($tagInfo, $val);
246 0         0 ++$count;
247 0         0 next;
248             }
249             # extract tags from this dictionary
250 5         10 my ($table, $rootID, $rootName);
251 5 50       11 if ($$tagInfo{SubDirectory}) {
252 5         14 $table = GetTagTable($$tagInfo{SubDirectory}{TagTable});
253             } else {
254 0         0 $table = $tagTablePtr;
255             # use hash ID and Name as base for contained tags to avoid conflicts
256 0         0 $rootID = $id;
257 0         0 $rootName = $$tagInfo{Name};
258             }
259 5         21 $count += ExtractTags($et, $val, $table, $rootID, $rootName, @index);
260             } else {
261             # handle this simple tag value
262 21         59 $et->HandleTag($tagTablePtr, $id, $val);
263 21         60 ++$count;
264             }
265             }
266 20 100       46 pop @index if defined $i;
267             }
268 6         24 return $count;
269             }
270              
271             #------------------------------------------------------------------------------
272             # Process BitTorrent file
273             # Inputs: 0) ExifTool object reference, 1) dirInfo reference (with RAF set)
274             # Returns: 1 on success, 0 if this wasn't a valid BitTorrent file
275             sub ProcessTorrent($$)
276             {
277 1     1 0 4 my ($et, $dirInfo) = @_;
278 1         3 my $success = 0;
279 1         3 my $raf = $$dirInfo{RAF};
280 1         3 my $buff = '';
281 1         6 pos($buff) = 0;
282 1         6 my $dict = ReadBencode($et, $raf, \$buff);
283 1         3 my $err = $$raf{BencodeError};
284 1 50       3 $et->Warn("Bencode error: $err") if $err;
285 1 50 33     8 if (ref $dict eq 'HASH' and ($$dict{announce} or $$dict{'created by'})) {
      33        
286 1         7 $et->SetFileType();
287 1         5 my $tagTablePtr = GetTagTable('Image::ExifTool::Torrent::Main');
288 1 50       5 ExtractTags($et, $dict, $tagTablePtr) and $success = 1;
289             }
290 1         16 return $success;
291             }
292              
293             1; # end
294              
295             __END__