File Coverage

blib/lib/Image/ExifTool/Torrent.pm
Criterion Covered Total %
statement 109 134 81.3
branch 54 94 57.4
condition 9 23 39.1
subroutine 8 8 100.0
pod 0 4 0.0
total 180 263 68.4


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