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