|
0 |
package Convert::BaseN;
|
|
1 |
use warnings;
|
|
2 |
use strict;
|
|
3 |
our $VERSION = sprintf "%d.%02d", q$Revision: 0.1 $ =~ /(\d+)/g;
|
|
4 |
use Carp;
|
|
5 |
|
|
6 |
sub _make_tr($$;$) {
|
|
7 |
my ( $from, $to, $opt ) = @_;
|
|
8 |
$opt ||= '';
|
|
9 |
my $tr = eval qq{ sub{ \$_[0] =~ tr#$from#$to#$opt } };
|
|
10 |
croak $@ if $@;
|
|
11 |
$tr;
|
|
12 |
}
|
|
13 |
|
|
14 |
my %h2q = qw{ 0 00 1 01 2 02 3 03 4 10 5 11 6 12 7 13
|
|
15 |
8 20 9 21 a 22 b 23 c 30 d 31 e 32 f 33 };
|
|
16 |
my %q2h = reverse %h2q;
|
|
17 |
|
|
18 |
my %o2b = qw{ 0 000 1 001 2 010 3 011 4 100 5 101 6 110 7 111 };
|
|
19 |
my %b2o = reverse %o2b;
|
|
20 |
|
|
21 |
my %v2b = do {
|
|
22 |
my $i = 0;
|
|
23 |
map { $_ => sprintf( "%05b", $i++ ) } ( '0' .. '9', 'A' .. 'V' );
|
|
24 |
};
|
|
25 |
my %b2v = reverse %v2b;
|
|
26 |
|
|
27 |
my %gen_decoders = (
|
|
28 |
2 => sub {
|
|
29 |
my ( $chars ) = @_;
|
|
30 |
my $tr = $chars ? _make_tr( $chars, '01' ) : undef;
|
|
31 |
sub {
|
|
32 |
my $str = shift;
|
|
33 |
$tr->($str) if $tr;
|
|
34 |
$str =~ tr/01//cd;
|
|
35 |
scalar pack "B*", $str;
|
|
36 |
}
|
|
37 |
},
|
|
38 |
4 => sub {
|
|
39 |
my ($chars) = @_;
|
|
40 |
my $tr = $chars ? _make_tr( $chars, '0123' ) : undef;
|
|
41 |
sub {
|
|
42 |
my $str = shift;
|
|
43 |
$tr->($str) if $tr;
|
|
44 |
$str =~ tr/0123//cd;
|
|
45 |
$str =~ s/(..)/$q2h{$1}/g;
|
|
46 |
scalar pack "H*", $str;
|
|
47 |
}
|
|
48 |
},
|
|
49 |
8 => sub {
|
|
50 |
my ($chars) = @_;
|
|
51 |
my $tr = $chars ? _make_tr( $chars, '0-7=' ) : undef;
|
|
52 |
sub {
|
|
53 |
my $str = shift;
|
|
54 |
$tr->($str) if $tr;
|
|
55 |
$str =~ tr/0-7//cd;
|
|
56 |
$str =~ s/(.)/$o2b{$1}/g;
|
|
57 |
my $padlen = (length $str) % 8;
|
|
58 |
$str =~ s/0{$padlen}\z//;
|
|
59 |
scalar pack "B*", $str;
|
|
60 |
}
|
|
61 |
},
|
|
62 |
16 => sub {
|
|
63 |
my ($chars) = @_;
|
|
64 |
my $tr = $chars ? _make_tr( $chars, '0-9a-f' ) : undef;
|
|
65 |
sub {
|
|
66 |
my $str = shift;
|
|
67 |
$tr->($str) if $tr;
|
|
68 |
$str =~ tr/0-9a-f//cd;
|
|
69 |
scalar pack "H*", lc $str;
|
|
70 |
}
|
|
71 |
},
|
|
72 |
32 => sub {
|
|
73 |
my ($chars) = @_;
|
|
74 |
my $tr = $chars ? _make_tr( $chars, '0-9A-V=' ) : undef;
|
|
75 |
sub {
|
|
76 |
my $str = shift;
|
|
77 |
$tr->($str) if $tr;
|
|
78 |
$str =~ tr/0-9A-V//cd;
|
|
79 |
$str =~ s/(.)/$v2b{$1}/g;
|
|
80 |
my $padlen = (length $str) % 8;
|
|
81 |
$str =~ s/0{$padlen}\z//;
|
|
82 |
scalar pack "B*", $str;
|
|
83 |
}
|
|
84 |
},
|
|
85 |
64 => sub {
|
|
86 |
require MIME::Base64;
|
|
87 |
my ($chars) = @_;
|
|
88 |
my $tr = $chars ? _make_tr( $chars, '0-9A-Za-z+/=' ) : undef;
|
|
89 |
sub {
|
|
90 |
my $str = shift;
|
|
91 |
$tr->($str) if $tr;
|
|
92 |
MIME::Base64::decode($str);
|
|
93 |
}
|
|
94 |
}
|
|
95 |
);
|
|
96 |
|
|
97 |
sub _fold_line {
|
|
98 |
my ( $str, $lf, $cpl ) = @_;
|
|
99 |
$lf = "\n" unless defined $lf;
|
|
100 |
# warn ord $lf;
|
|
101 |
return $str unless $lf;
|
|
102 |
$cpl ||= 76;
|
|
103 |
$str =~ s/(.{$cpl})/$1$lf/gms;
|
|
104 |
$str;
|
|
105 |
}
|
|
106 |
|
|
107 |
my %gen_encoders = (
|
|
108 |
2 => sub {
|
|
109 |
my ($chars) = @_;
|
|
110 |
my $tr = $chars ? _make_tr( '01', $chars ) : undef;
|
|
111 |
sub ($;$$) {
|
|
112 |
my ( $str, $lf, $cpl ) = @_;
|
|
113 |
my $ret = unpack "B*", $str;
|
|
114 |
$tr->($ret) if $tr;
|
|
115 |
_fold_line( $ret, $lf, $cpl );
|
|
116 |
}
|
|
117 |
},
|
|
118 |
4 => sub {
|
|
119 |
my ($chars) = @_;
|
|
120 |
my $tr = $chars ? _make_tr( '0123', $chars ) : undef;
|
|
121 |
sub ($;$) {
|
|
122 |
my ( $str, $lf, $cpl ) = @_;
|
|
123 |
my $ret = unpack "H*", $str;
|
|
124 |
$ret =~ s/(.)/$h2q{$1}/g;
|
|
125 |
$tr->($ret) if $tr;
|
|
126 |
_fold_line( $ret, $lf, $cpl );
|
|
127 |
}
|
|
128 |
},
|
|
129 |
8 => sub {
|
|
130 |
my ( $chars, $nopad ) = @_;
|
|
131 |
my $tr = $chars ? _make_tr( '0-7=', $chars ) : undef;
|
|
132 |
sub ($;$$) {
|
|
133 |
my ( $str, $lf, $cpl ) = @_;
|
|
134 |
my $ret = unpack "B*", $str;
|
|
135 |
$ret .= 0 while ( length $ret ) % 3;
|
|
136 |
$ret =~ s/(...)/$b2o{$1}/g;
|
|
137 |
$nopad or do{ $ret .= '=' while ( length $ret ) % 8 };
|
|
138 |
$tr->($ret) if $tr;
|
|
139 |
_fold_line( $ret, $lf, $cpl );
|
|
140 |
}
|
|
141 |
},
|
|
142 |
16 => sub {
|
|
143 |
my ($chars) = @_;
|
|
144 |
my $tr = $chars ? _make_tr( '0-9a-f', $chars ) : undef;
|
|
145 |
sub ($;$$) {
|
|
146 |
my ( $str, $lf, $cpl ) = @_;
|
|
147 |
my $ret = unpack "H*", $str;
|
|
148 |
$tr->($ret) if $tr;
|
|
149 |
_fold_line( $ret, $lf, $cpl );
|
|
150 |
}
|
|
151 |
},
|
|
152 |
32 => sub {
|
|
153 |
my ( $chars, $nopad ) = @_;
|
|
154 |
my $tr = $chars ? _make_tr( '0-9A-V=', $chars ) : undef;
|
|
155 |
sub ($;$$) {
|
|
156 |
my ( $str, $lf, $cpl ) = @_;
|
|
157 |
my $ret = unpack "B*", $str;
|
|
158 |
$ret .= 0 while ( length $ret ) % 5;
|
|
159 |
$ret =~ s/(.....)/$b2v{$1}/g;
|
|
160 |
$nopad or do{ $ret .= '=' while ( length $ret ) % 8 };
|
|
161 |
$tr->($ret) if $tr;
|
|
162 |
_fold_line( $ret, $lf, $cpl );
|
|
163 |
}
|
|
164 |
},
|
|
165 |
64 => sub {
|
|
166 |
require MIME::Base64;
|
|
167 |
my ( $chars, $nopad ) = @_;
|
|
168 |
my $tr = $chars ? _make_tr( '0-9A-Za-z+/=', $chars ) : undef;
|
|
169 |
sub ($;$$) {
|
|
170 |
my ( $str, $lf, $cpl ) = @_;
|
|
171 |
$str =
|
|
172 |
defined $lf
|
|
173 |
? _fold_line( MIME::Base64::encode( $str, '' ), $lf, $cpl )
|
|
174 |
: MIME::Base64::encode( $str, $lf );
|
|
175 |
$str =~ tr/=//d if $nopad;
|
|
176 |
$tr->($str) if $tr;
|
|
177 |
$str;
|
|
178 |
}
|
|
179 |
}
|
|
180 |
);
|
|
181 |
|
|
182 |
sub _base64_decode_any {
|
|
183 |
require MIME::Base64;
|
|
184 |
my $str = shift;
|
|
185 |
$str =~ tr{\-\_\+\,\[\]}{+/+/+/};
|
|
186 |
local $^W = 0; # in case the string is not padded
|
|
187 |
MIME::Base64::decode($str);
|
|
188 |
}
|
|
189 |
|
|
190 |
|
|
191 |
our %named_decoder = (
|
|
192 |
base2 => $gen_decoders{2}->(),
|
|
193 |
base4 => $gen_decoders{4}->(),
|
|
194 |
DNA => $gen_decoders{4}->('ACGT'),
|
|
195 |
RNA => $gen_decoders{4}->('UGCA'),
|
|
196 |
base8 => $gen_decoders{8}->(),
|
|
197 |
base16 => $gen_decoders{16}->('0-9A-F'),
|
|
198 |
base32 => $gen_decoders{32}->('A-Z2-7='),
|
|
199 |
base32hex => $gen_decoders{32}->(),
|
|
200 |
base64 => \&_base64_decode_any,
|
|
201 |
base64_url => \&_base64_decode_any,
|
|
202 |
base64_imap => \&_base64_decode_any,
|
|
203 |
base64_ircu => \&_base64_decode_any,
|
|
204 |
);
|
|
205 |
|
|
206 |
our %named_encoder = (
|
|
207 |
base2 => $gen_encoders{2}->(),
|
|
208 |
base4 => $gen_encoders{4}->(),
|
|
209 |
DNA => $gen_encoders{4}->('ACGT'),
|
|
210 |
RNA => $gen_encoders{4}->('UGCA'),
|
|
211 |
base8 => $gen_encoders{8}->(),
|
|
212 |
base16 => $gen_encoders{16}->('0-9A-F'),
|
|
213 |
base32 => $gen_encoders{32}->('A-Z2-7='),
|
|
214 |
base32hex => $gen_encoders{32}->(),
|
|
215 |
base64 => $gen_encoders{64}->(),
|
|
216 |
base64_url => $gen_encoders{64}->( '0-9A-Za-z\-\_=', 1 ),
|
|
217 |
base64_imap => $gen_encoders{64}->('0-9A-Za-z\+\,='),
|
|
218 |
base64_ircu => $gen_encoders{64}->('0-9A-Za-z\[\]='),
|
|
219 |
);
|
|
220 |
|
|
221 |
sub new {
|
|
222 |
my $pkg = shift;
|
|
223 |
my %opt = @_ == 1 ? ( name => shift ) : @_;
|
|
224 |
my ( $encoder, $decoder );
|
|
225 |
if ( $opt{name} ) {
|
|
226 |
$decoder = $named_decoder{ $opt{name} };
|
|
227 |
$encoder = $named_encoder{ $opt{name} };
|
|
228 |
croak "$opt{name} unknown" unless $decoder and $encoder;
|
|
229 |
}
|
|
230 |
else {
|
|
231 |
eval {
|
|
232 |
my $nopad = exists $opt{padding} ? !$opt{padding}
|
|
233 |
: $opt{nopadding};
|
|
234 |
$decoder = $gen_decoders{ $opt{base} }->( $opt{chars} );
|
|
235 |
$encoder = $gen_encoders{ $opt{base} }->( $opt{chars}, $nopad );
|
|
236 |
};
|
|
237 |
croak "base $opt{base} unknown" if $@;
|
|
238 |
}
|
|
239 |
bless {
|
|
240 |
decoder => $decoder,
|
|
241 |
encoder => $encoder,
|
|
242 |
}, $pkg;
|
|
243 |
}
|
|
244 |
|
|
245 |
sub decode { my $self = shift; $self->{decoder}->(@_) }
|
|
246 |
sub encode { my $self = shift; $self->{encoder}->(@_) }
|
|
247 |
|
|
248 |
if (__FILE__ eq $0){
|
|
249 |
my ($bn, $encoded);
|
|
250 |
|
|
251 |
$bn = __PACKAGE__->new(base => 2, chars => '<>');
|
|
252 |
$encoded = $bn->encode("dankogai", " ");
|
|
253 |
warn $encoded;
|
|
254 |
warn $bn->decode($encoded);
|
|
255 |
|
|
256 |
$bn = __PACKAGE__->new(base => 4, chars => 'ACGT');
|
|
257 |
$encoded = $bn->encode("dankogai", " ");
|
|
258 |
warn $encoded;
|
|
259 |
warn $bn->decode($encoded);
|
|
260 |
$bn = __PACKAGE__->new(base => 8, chars => 'abcdefgh=');
|
|
261 |
$encoded = $bn->encode("dankogai");
|
|
262 |
warn $encoded;
|
|
263 |
warn $bn->decode($encoded);
|
|
264 |
warn length $bn->decode($encoded);
|
|
265 |
|
|
266 |
$bn = __PACKAGE__->new(base => 16, chars => '0-9A-F');
|
|
267 |
$encoded = $bn->encode("dankogai", " ");
|
|
268 |
warn $encoded;
|
|
269 |
|
|
270 |
$bn = __PACKAGE__->new(base => 32);
|
|
271 |
$encoded = $bn->encode("dankogai");
|
|
272 |
warn $encoded;
|
|
273 |
warn $bn->decode($encoded);
|
|
274 |
warn length $bn->decode($encoded);
|
|
275 |
|
|
276 |
$bn = __PACKAGE__->new(base => 32, chars => 'A-Z2-7=');
|
|
277 |
$encoded = $bn->encode("dankogai");
|
|
278 |
warn $encoded;
|
|
279 |
warn $bn->decode($encoded);
|
|
280 |
warn length $bn->decode($encoded);
|
|
281 |
|
|
282 |
$bn = __PACKAGE__->new(base => 64);
|
|
283 |
$encoded = $bn->encode("dankogai");
|
|
284 |
warn $encoded;
|
|
285 |
warn $bn->decode($encoded);
|
|
286 |
|
|
287 |
$bn = __PACKAGE__->new(base => 64,chars => '0-9A-Za-z\-_=');
|
|
288 |
$encoded = $bn->encode(join("", map {chr} 0x21 .. 0x7e), "\n", 40);
|
|
289 |
warn $encoded;
|
|
290 |
warn $bn->decode($encoded);
|
|
291 |
warn scalar unpack "H*", $bn->decode('-__-');
|
|
292 |
|
|
293 |
$bn = __PACKAGE__->new('base69');
|
|
294 |
#warn $bn->encode("dankogai");
|
|
295 |
#$bn = __PACKAGE__->new(name => 'base4');
|
|
296 |
#$bn = __PACKAGE__->new(name => 'basex');
|
|
297 |
#$bn = __PACKAGE__->new(base => 17);
|
|
298 |
}
|
|
299 |
|
|
300 |
1; # End of Convert::BaseN
|
|
301 |
|
|
302 |
=head1 NAME
|
|
303 |
|
|
304 |
Convert::BaseN - encoding and decoding of base{2,4,8,16,32,64} strings
|
|
305 |
|
|
306 |
=head1 VERSION
|
|
307 |
|
|
308 |
$Id: BaseN.pm,v 0.1 2008/06/16 17:34:27 dankogai Exp dankogai $
|
|
309 |
|
|
310 |
=cut
|
|
311 |
|
|
312 |
=head1 SYNOPSIS
|
|
313 |
|
|
314 |
use Convert::BaseN;
|
|
315 |
# by name
|
|
316 |
my $cb = Convert::BaseN->new('base64');
|
|
317 |
my $cb = Convert::BaseN->new( name => 'base64' );
|
|
318 |
# or base
|
|
319 |
my $cb = Convert::BaseN->new( base => 64 );
|
|
320 |
my $cb_url = Convert::BaseN->new(
|
|
321 |
base => 64,
|
|
322 |
chars => '0-9A-Za-z\-_='
|
|
323 |
);
|
|
324 |
# encode and decode
|
|
325 |
$encoded = $cb->encode($data);
|
|
326 |
$decoded = $cb->decode($encoded);
|
|
327 |
|
|
328 |
=head1 EXPORT
|
|
329 |
|
|
330 |
Nothing. Instead of that, this module builds I<transcoder object> for
|
|
331 |
you and you use its C<decode> and C<encode> methods to get the job
|
|
332 |
done.
|
|
333 |
|
|
334 |
=head1 FUNCTIONS
|
|
335 |
|
|
336 |
=head2 new
|
|
337 |
|
|
338 |
Create the transcoder object.
|
|
339 |
|
|
340 |
# by name
|
|
341 |
my $cb = Convert::BaseN->new('base64');
|
|
342 |
my $cb = Convert::BaseN->new( name => 'base64' );
|
|
343 |
# or base
|
|
344 |
my $cb = Convert::BaseN->new( base => 64 );
|
|
345 |
my $cb_url = Convert::BaseN->new(
|
|
346 |
base => 64,
|
|
347 |
chars => '0-9A-Za-z\-_='
|
|
348 |
);
|
|
349 |
|
|
350 |
You can pick the decoder by name or create your own by specifying base
|
|
351 |
and character map.
|
|
352 |
|
|
353 |
=over 2
|
|
354 |
|
|
355 |
=item base
|
|
356 |
|
|
357 |
Must be 2, 4, 16, 32 or 64.
|
|
358 |
|
|
359 |
=item chars
|
|
360 |
|
|
361 |
Specifiles the character map. The format is the same as C<tr>.
|
|
362 |
|
|
363 |
# DNA is coded that way.
|
|
364 |
my $dna = Convert::BaseN->new( base => 4, chars => 'ACGT' );
|
|
365 |
|
|
366 |
=item padding
|
|
367 |
|
|
368 |
=item nopadding
|
|
369 |
|
|
370 |
Specifies if padding (adding '=' or other chars) is required when
|
|
371 |
encoding. default is yes.
|
|
372 |
|
|
373 |
# url-safe Base64
|
|
374 |
my $b64url = Convert::BaseN->new(
|
|
375 |
base => 64, chars => '0-9A-Za-z\-_=', padding => 0;
|
|
376 |
);
|
|
377 |
|
|
378 |
=item name
|
|
379 |
|
|
380 |
When specified, the following pre-defined encodings will be used.
|
|
381 |
|
|
382 |
=over 2
|
|
383 |
|
|
384 |
=item base2
|
|
385 |
|
|
386 |
base 2 encoding. C<perl> is C<01110000011001010111001001101100>.
|
|
387 |
|
|
388 |
=item base4
|
|
389 |
|
|
390 |
=item DNA
|
|
391 |
|
|
392 |
=item RNA
|
|
393 |
|
|
394 |
base 4 encodings. C<perl> is:
|
|
395 |
|
|
396 |
base4: 1300121113021230
|
|
397 |
DNA: CTAACGCCCTAGCGTA
|
|
398 |
RNA: GAUUGCGGGAUCGCAU
|
|
399 |
|
|
400 |
base 16 encoding. C<perl> is C<7065726c>.
|
|
401 |
|
|
402 |
=item base32
|
|
403 |
|
|
404 |
=item base32hex
|
|
405 |
|
|
406 |
base 32 encoding mentioned in RFC4648. C<perl> is:
|
|
407 |
|
|
408 |
base32: OBSXE3A==
|
|
409 |
base32hex: E1IN4R0==
|
|
410 |
|
|
411 |
=item base64
|
|
412 |
|
|
413 |
=item base64_url
|
|
414 |
|
|
415 |
=item base64_imap
|
|
416 |
|
|
417 |
=item base64_ircu
|
|
418 |
|
|
419 |
base 64 encoding, as in L<MIME::Base64>. They differ only in
|
|
420 |
characters to represent number 62 and 63 as follows.
|
|
421 |
|
|
422 |
base64: +/
|
|
423 |
base64_url: -_
|
|
424 |
base64_imap: +,
|
|
425 |
base64_ircu: []
|
|
426 |
|
|
427 |
for all predefined base 64 variants, C<decode> accept ANY form of those.
|
|
428 |
|
|
429 |
=back
|
|
430 |
|
|
431 |
=back
|
|
432 |
|
|
433 |
=head2 decode
|
|
434 |
|
|
435 |
Does decode
|
|
436 |
|
|
437 |
my $decoded = $cb->decode($data)
|
|
438 |
|
|
439 |
=head2 encode
|
|
440 |
|
|
441 |
Does encode.
|
|
442 |
|
|
443 |
# line folds every 76 octets, like MIME::Base64::encode
|
|
444 |
my $encoded = $cb->encode($data);
|
|
445 |
# no line folding (compatibile w/ MIME::Base64)
|
|
446 |
my $encoded = $cb->encode($data, "");
|
|
447 |
# line folding by CRLF, every 40 octets
|
|
448 |
my $encoded = $cb->encode($data, "\r\n", 40);
|
|
449 |
|
|
450 |
=head1 SEE ALSO
|
|
451 |
|
|
452 |
RFC4648 L<http://tools.ietf.org/html/rfc4648>
|
|
453 |
|
|
454 |
Wikipedia L<http://en.wikipedia.org/wiki/Base64>
|
|
455 |
|
|
456 |
L<http://www.centricorp.com/papers/base64.htm>
|
|
457 |
|
|
458 |
L<MIME::Base64>
|
|
459 |
|
|
460 |
L<MIME::Base32>
|
|
461 |
|
|
462 |
L<MIME::Base64::URLSafe>
|
|
463 |
|
|
464 |
=head1 AUTHOR
|
|
465 |
|
|
466 |
Dan Kogai, C<< <dankogai at dan.co.jp> >>
|
|
467 |
|
|
468 |
=head1 BUGS
|
|
469 |
|
|
470 |
Please report any bugs or feature requests to C<bug-convert-basen at rt.cpan.org>, or through
|
|
471 |
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Convert-BaseN>. I will be notified, and then you'll
|
|
472 |
automatically be notified of progress on your bug as I make changes.
|
|
473 |
|
|
474 |
=head1 SUPPORT
|
|
475 |
|
|
476 |
You can find documentation for this module with the perldoc command.
|
|
477 |
|
|
478 |
perldoc Convert::BaseN
|
|
479 |
|
|
480 |
You can also look for information at:
|
|
481 |
|
|
482 |
=over 4
|
|
483 |
|
|
484 |
=item * RT: CPAN's request tracker
|
|
485 |
|
|
486 |
L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Convert-BaseN>
|
|
487 |
|
|
488 |
=item * AnnoCPAN: Annotated CPAN documentation
|
|
489 |
|
|
490 |
L<http://annocpan.org/dist/Convert-BaseN>
|
|
491 |
|
|
492 |
=item * CPAN Ratings
|
|
493 |
|
|
494 |
L<http://cpanratings.perl.org/d/Convert-BaseN>
|
|
495 |
|
|
496 |
=item * Search CPAN
|
|
497 |
|
|
498 |
L<http://search.cpan.org/dist/Convert-BaseN>
|
|
499 |
|
|
500 |
=back
|
|
501 |
|
|
502 |
=head1 ACKNOWLEDGEMENTS
|
|
503 |
|
|
504 |
N/A
|
|
505 |
|
|
506 |
=head1 COPYRIGHT & LICENSE
|
|
507 |
|
|
508 |
Copyright 2008 Dan Kogai, all rights reserved.
|
|
509 |
|
|
510 |
This program is free software; you can redistribute it and/or modify it
|
|
511 |
under the same terms as Perl itself.
|
|
512 |
|
|
513 |
|
|
514 |
=cut
|
|
515 |
|
|
516 |
|