File Coverage

blib/lib/Image/ExifTool/Torrent.pm
Criterion Covered Total %
statement 106 131 80.9
branch 53 94 56.3
condition 8 23 34.7
subroutine 7 7 100.0
pod 0 4 0.0
total 174 259 67.1


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   5626 use strict;
  1         2  
  1         34  
14 1     1   3 use vars qw($VERSION);
  1         2  
  1         36  
15 1     1   3 use Image::ExifTool qw(:DataAccess :Utils);
  1         2  
  1         1935  
16              
17             $VERSION = '1.07';
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 2 my ($raf, $dataPt) = @_;
93 1         2 my $buf2;
94 1         6 my $n = $raf->Read($buf2, 65536);
95 1 50       7 $$raf{BencodeEOF} = 1 if $n != 65536;
96 1 50       13 $$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 173 my ($et, $raf, $dataPt) = @_;
108              
109             # read more if necessary (keep a minimum of 64 bytes in the buffer)
110 70         113 my $pos = pos($$dataPt);
111 70 50       172 return undef unless defined $pos;
112 70         128 my $remaining = length($$dataPt) - $pos;
113 70 100 100     174 ReadMore($raf, $dataPt) if $remaining < 64 and not $$raf{BencodeEOF};
114              
115             # read next token
116 70 50       230 $$dataPt =~ /(.)/sg or return undef;
117              
118 70         106 my $val;
119 70         165 my $tok = $1;
120 70 100 33     421 if ($tok eq 'i') { # integer
    100          
    100          
    100          
    50          
121 6 50       34 $$dataPt =~ /\G(-?\d+)e/g or return $val;
122 6         12 $val = $1;
123             } elsif ($tok eq 'd') { # dictionary
124 6         12 $val = { };
125 6         11 for (;;) {
126 26         67 my $k = ReadBencode($et, $raf, $dataPt);
127 26 100       66 last unless defined $k;
128             # the key must be a byte string
129 20 50       45 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         46 my $v = ReadBencode($et, $raf, $dataPt);
134 20 50       44 last unless defined $v;
135 20         67 $$val{$k} = $v;
136             }
137             } elsif ($tok eq 'l') { # list
138 8         16 $val = [ ];
139 8         14 for (;;) {
140 23         61 my $v = ReadBencode($et, $raf, $dataPt);
141 23 100       73 last unless defined $v;
142 15         40 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         121 my $len = $tok . $1;
148 36         80 my $more = $len - (length($$dataPt) - pos($$dataPt));
149 36         57 my $value;
150 36 50       72 if ($more <= 0) {
    0          
151 36         87 $value = substr($$dataPt,pos($$dataPt),$len);
152 36         103 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       79 if (defined $value) {
    0          
167             # return as binary data unless it is a reasonable-length ASCII string
168 36 50       132 if (length($value) > 256) {
    100          
169 0         0 $val = \$value;
170             } elsif ($value =~ /[^\t\x20-\x7e]/) {
171 1 50       8 if (Image::ExifTool::IsUTF8(\$value) >= 0) {
172 0         0 $val = $et->Decode($value, 'UTF8');
173             } else {
174 1         3 $val = \$value;
175             }
176             } else {
177 35         68 $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         173 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 21 my ($et, $hashPtr, $tagTablePtr, $baseID, $baseName, @index) = @_;
196 6         12 my $count = 0;
197 6         10 my $tag;
198 6         36 foreach $tag (sort keys %$hashPtr) {
199 20         74 my $val = $$hashPtr{$tag};
200 20         41 my ($i, $j, @more);
201 20         64 for (; defined $val; $val = shift @more) {
202 27 50       63 my $id = defined $baseID ? "$baseID/$tag" : $tag;
203 27 50       75 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       101 my $tagInfo = $et->GetTagInfo($tagTablePtr, $id) or next;
213 27 100       80 if (ref $val eq 'ARRAY') {
214 8 100       26 if ($$tagInfo{JoinPath}) {
215 4 50       12 $val = join '/', map { ref $_ ? '(Binary data)' : $_ } @$val;
  5         44  
216             } else {
217 4         14 push @more, @$val;
218 4 100       13 next if ref $more[0] eq 'ARRAY'; # continue expanding nested lists
219 3         6 $val = shift @more;
220 3 50       11 $i or $i = 0, push(@index, $i);
221             }
222             }
223 26 100       67 $index[-1] = ++$i if defined $i;
224 26 100       61 if (@index) {
225 17         54 $id .= join '_', @index; # add instance number(s) to tag ID
226 17 50       43 unless ($$tagTablePtr{$id}) {
227 17         36 my $name = $$tagInfo{Name};
228             # embed indices at position of '1' in tag name
229 17         45 my $n = ($name =~ tr/1/#/);
230 17         46 for ($j=0; $j<$n; ++$j) {
231 13   50     38 my $idx = $index[$j] || '';
232 13         89 $name =~ s/#/$idx/;
233             }
234             # put remaining indices at end of tag name
235 17         45 for (; $j<@index; ++$j) {
236 4 50       21 $name .= '_' if $name =~ /\d$/;
237 4         15 $name .= $index[$j];
238             }
239 17         171 AddTagToTable($tagTablePtr, $id, { %$tagInfo, Name => $name });
240             }
241 17 50       69 $tagInfo = $et->GetTagInfo($tagTablePtr, $id) or next;
242             }
243 26 100       68 if (ref $val eq 'HASH') {
244 5 0 33     22 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         26 my ($table, $rootID, $rootName);
251 5 50       18 if ($$tagInfo{SubDirectory}) {
252 5         21 $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         30 $count += ExtractTags($et, $val, $table, $rootID, $rootName, @index);
260             } else {
261             # handle this simple tag value
262 21         84 $et->HandleTag($tagTablePtr, $id, $val);
263 21         105 ++$count;
264             }
265             }
266 20 100       60 pop @index if defined $i;
267             }
268 6         36 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 3 my ($et, $dirInfo) = @_;
278 1         2 my $success = 0;
279 1         4 my $raf = $$dirInfo{RAF};
280 1         3 my $buff = '';
281 1         5 pos($buff) = 0;
282 1         6 my $dict = ReadBencode($et, $raf, \$buff);
283 1         3 my $err = $$raf{BencodeError};
284 1 50       5 $et->Warn("Bencode error: $err") if $err;
285 1 0 0     27 if (ref $dict eq 'HASH' and ($$dict{announce} or $$dict{'created by'} or $$dict{info})) {
      33        
286 1         10 $et->SetFileType();
287 1         5 my $tagTablePtr = GetTagTable('Image::ExifTool::Torrent::Main');
288 1 50       6 ExtractTags($et, $dict, $tagTablePtr) and $success = 1;
289             }
290 1         15 return $success;
291             }
292              
293             1; # end
294              
295             __END__