ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Net-FCP/FCP.pm
(Generate patch)

Comparing Net-FCP/FCP.pm (file contents):
Revision 1.13 by root, Wed Sep 10 05:06:16 2003 UTC vs.
Revision 1.20 by root, Mon Sep 15 00:05:32 2003 UTC

35The import tag to use is named C<event=xyz>, e.g. C<event=Event>, 35The import tag to use is named C<event=xyz>, e.g. C<event=Event>,
36C<event=Glib> etc. 36C<event=Glib> etc.
37 37
38You should specify the event module to use only in the main program. 38You should specify the event module to use only in the main program.
39 39
40If no event model has been specified, FCP tries to autodetect it on first
41use (e.g. first transaction), in this order: Coro, Event, Glib, Tk.
42
43=head2 FREENET BASICS
44
45Ok, this section will not explain any freenet basics to you, just some
46problems I found that you might want to avoid:
47
48=over 4
49
50=item freenet URIs are _NOT_ URIs
51
52Whenever a "uri" is required by the protocol, freenet expects a kind of
53URI prefixed with the "freenet:" scheme, e.g. "freenet:CHK...". However,
54these are not URIs, as freeent fails to parse them correctly, that is, you
55must unescape an escaped characters ("%2c" => ",") yourself. Maybe in the
56future this library will do it for you, so watch out for this incompatible
57change.
58
59=item Numbers are in HEX
60
61Virtually every number in the FCP protocol is in hex. Be sure to use
62C<hex()> on all such numbers, as the module (currently) does nothing to
63convert these for you.
64
65=back
66
40=head2 THE Net::FCP CLASS 67=head2 THE Net::FCP CLASS
41 68
42=over 4 69=over 4
43 70
44=cut 71=cut
45 72
46package Net::FCP; 73package Net::FCP;
47 74
48use Carp; 75use Carp;
49 76
50$VERSION = 0.05; 77$VERSION = 0.08;
51 78
52no warnings; 79no warnings;
53 80
54our $EVENT = Net::FCP::Event::Auto::; 81our $EVENT = Net::FCP::Event::Auto::;
55$EVENT = Net::FCP::Event::Event;#d#
56 82
57sub import { 83sub import {
58 shift; 84 shift;
59 85
60 for (@_) { 86 for (@_) {
61 if (/^event=(\w+)$/) { 87 if (/^event=(\w+)$/) {
62 $EVENT = "Net::FCP::Event::$1"; 88 $EVENT = "Net::FCP::Event::$1";
89 eval "require $EVENT";
63 } 90 }
64 } 91 }
65 eval "require $EVENT";
66 die $@ if $@; 92 die $@ if $@;
67} 93}
68 94
69sub touc($) { 95sub touc($) {
70 local $_ = shift; 96 local $_ = shift;
93 119
94 ( 120 (
95 version => { revision => 1 }, 121 version => { revision => 1 },
96 document => [ 122 document => [
97 { 123 {
98 "info.format" => "image/jpeg", 124 info => { format" => "image/jpeg" },
99 name => "background.jpg", 125 name => "background.jpg",
100 "redirect.target" => "freenet:CHK\@ZcagI,ra726bSw" 126 redirect => { target => "freenet:CHK\@ZcagI,ra726bSw" },
101 }, 127 },
102 { 128 {
103 "info.format" => "text/html", 129 info => { format" => "text/html" },
104 name => ".next", 130 name => ".next",
105 "redirect.target" => "freenet:SSK\@ilUPAgM/TFEE/3" 131 redirect => { target => "freenet:SSK\@ilUPAgM/TFEE/3" },
106 }, 132 },
107 { 133 {
108 "info.format" => "text/html", 134 info => { format" => "text/html" },
109 "redirect.target" => "freenet:CHK\@8M8Po8ucwI,8xA" 135 redirect => { target => "freenet:CHK\@8M8Po8ucwI,8xA" },
110 } 136 }
111 ] 137 ]
112 ) 138 )
113 139
114=cut 140=cut
125 my ($k, $v) = ($1, $2); 151 my ($k, $v) = ($1, $2);
126 my @p = split /\./, tolc $k, 3; 152 my @p = split /\./, tolc $k, 3;
127 153
128 $hdr->{$p[0]} = $v if @p == 1; # lamest code I ever wrote 154 $hdr->{$p[0]} = $v if @p == 1; # lamest code I ever wrote
129 $hdr->{$p[0]}{$p[1]} = $v if @p == 2; 155 $hdr->{$p[0]}{$p[1]} = $v if @p == 2;
130 $hdr->{$p[0]}{$p[1]}{$p[3]} = $v if @p == 3; 156 $hdr->{$p[0]}{$p[1]}{$p[2]} = $v if @p == 3;
131 die "FATAL: 4+ dot metadata" if @p >= 4; 157 die "FATAL: 4+ dot metadata" if @p >= 4;
132 } 158 }
133 159
134 if ($data =~ /\GEndPart\015?\012/gc) { 160 if ($data =~ /\GEndPart\015?\012/gc) {
135 # nop 161 # nop
136 } elsif ($data =~ /\GEnd\015?\012/gc) { 162 } elsif ($data =~ /\GEnd(\015?\012|$)/gc) {
137 last; 163 last;
138 } elsif ($data =~ /\G([A-Za-z0-9.\-]+)\015?\012/gcs) { 164 } elsif ($data =~ /\G([A-Za-z0-9.\-]+)\015?\012/gcs) {
139 push @{$meta->{tolc $1}}, $hdr = {}; 165 push @{$meta->{tolc $1}}, $hdr = {};
140 } elsif ($data =~ /\G(.*)/gcs) { 166 } elsif ($data =~ /\G(.*)/gcs) {
167 print STDERR "metadata format error ($1), please report this string: <<$data>>";
141 die "metadata format error ($1)"; 168 die "metadata format error";
142 } 169 }
143 } 170 }
144 } 171 }
145 172
146 #$meta->{tail} = substr $data, pos $data; 173 #$meta->{tail} = substr $data, pos $data;
152 179
153Create a new virtual FCP connection to the given host and port (default 180Create a new virtual FCP connection to the given host and port (default
154127.0.0.1:8481, or the environment variables C<FREDHOST> and C<FREDPORT>). 181127.0.0.1:8481, or the environment variables C<FREDHOST> and C<FREDPORT>).
155 182
156Connections are virtual because no persistent physical connection is 183Connections are virtual because no persistent physical connection is
184established.
185
186=begin comment
187
157established. However, the existance of the node is checked by executing a 188However, the existance of the node is checked by executing a
158C<ClientHello> transaction. 189C<ClientHello> transaction.
190
191=end
159 192
160=cut 193=cut
161 194
162sub new { 195sub new {
163 my $class = shift; 196 my $class = shift;
172 $self; 205 $self;
173} 206}
174 207
175sub progress { 208sub progress {
176 my ($self, $txn, $type, $attr) = @_; 209 my ($self, $txn, $type, $attr) = @_;
177 warn "progress<$txn,$type," . (join ":", %$attr) . ">\n"; 210 #warn "progress<$txn,$type," . (join ":", %$attr) . ">\n";
178} 211}
179 212
180=item $txn = $fcp->txn(type => attr => val,...) 213=item $txn = $fcp->txn(type => attr => val,...)
181 214
182The low-level interface to transactions. Don't use it. 215The low-level interface to transactions. Don't use it.
213 my $txn = "Net::FCP::Txn::$type"->new(fcp => $self, type => tolc $type, attr => \%attr); 246 my $txn = "Net::FCP::Txn::$type"->new(fcp => $self, type => tolc $type, attr => \%attr);
214 247
215 $txn; 248 $txn;
216} 249}
217 250
218sub _txn($&) { 251{ # transactions
252
253my $txn = sub {
219 my ($name, $sub) = @_; 254 my ($name, $sub) = @_;
220 *{"$name\_txn"} = $sub; 255 *{"txn_$name"} = $sub;
221 *{$name} = sub { $sub->(@_)->result }; 256 *{$name} = sub { $sub->(@_)->result };
222} 257};
223 258
224=item $txn = $fcp->txn_client_hello 259=item $txn = $fcp->txn_client_hello
225 260
226=item $nodehello = $fcp->client_hello 261=item $nodehello = $fcp->client_hello
227 262
233 protocol => "1.2", 268 protocol => "1.2",
234 } 269 }
235 270
236=cut 271=cut
237 272
238_txn client_hello => sub { 273$txn->(client_hello => sub {
239 my ($self) = @_; 274 my ($self) = @_;
240 275
241 $self->txn ("client_hello"); 276 $self->txn ("client_hello");
242}; 277});
243 278
244=item $txn = $fcp->txn_client_info 279=item $txn = $fcp->txn_client_info
245 280
246=item $nodeinfo = $fcp->client_info 281=item $nodeinfo = $fcp->client_info
247 282
271 routing_time => "a5", 306 routing_time => "a5",
272 } 307 }
273 308
274=cut 309=cut
275 310
276_txn client_info => sub { 311$txn->(client_info => sub {
277 my ($self) = @_; 312 my ($self) = @_;
278 313
279 $self->txn ("client_info"); 314 $self->txn ("client_info");
280}; 315});
281 316
282=item $txn = $fcp->txn_generate_chk ($metadata, $data) 317=item $txn = $fcp->txn_generate_chk ($metadata, $data)
283 318
284=item $uri = $fcp->generate_chk ($metadata, $data) 319=item $uri = $fcp->generate_chk ($metadata, $data)
285 320
286Creates a new CHK, given the metadata and data. UNTESTED. 321Creates a new CHK, given the metadata and data. UNTESTED.
287 322
288=cut 323=cut
289 324
290_txn generate_chk => sub { 325$txn->(generate_chk => sub {
291 my ($self, $metadata, $data) = @_; 326 my ($self, $metadata, $data) = @_;
292 327
293 $self->txn (generate_chk => data => "$data$metadata", metadata_length => length $metadata); 328 $self->txn (generate_chk => data => "$metadata$data", metadata_length => length $metadata);
294}; 329});
295 330
296=item $txn = $fcp->txn_generate_svk_pair 331=item $txn = $fcp->txn_generate_svk_pair
297 332
298=item ($public, $private) = @{ $fcp->generate_svk_pair } 333=item ($public, $private) = @{ $fcp->generate_svk_pair }
299 334
304 "ZnmvMITaTXBMFGl4~jrjuyWxOWg" 339 "ZnmvMITaTXBMFGl4~jrjuyWxOWg"
305 ] 340 ]
306 341
307=cut 342=cut
308 343
309_txn generate_svk_pair => sub { 344$txn->(generate_svk_pair => sub {
310 my ($self) = @_; 345 my ($self) = @_;
311 346
312 $self->txn ("generate_svk_pair"); 347 $self->txn ("generate_svk_pair");
313}; 348});
314 349
315=item $txn = $fcp->txn_insert_private_key ($private) 350=item $txn = $fcp->txn_insert_private_key ($private)
316 351
317=item $uri = $fcp->insert_private_key ($private) 352=item $public = $fcp->insert_private_key ($private)
318 353
319Inserts a private key. $private can be either an insert URI (must start 354Inserts a private key. $private can be either an insert URI (must start
320with freenet:SSK@) or a raw private key (i.e. the private value you get back 355with C<freenet:SSK@>) or a raw private key (i.e. the private value you get
321from C<generate_svk_pair>). 356back from C<generate_svk_pair>).
322 357
323Returns the public key. 358Returns the public key.
324 359
325UNTESTED. 360UNTESTED.
326 361
327=cut 362=cut
328 363
329_txn insert_private_key => sub { 364$txn->(insert_private_key => sub {
330 my ($self, $privkey) = @_; 365 my ($self, $privkey) = @_;
331 366
332 $self->txn (invert_private_key => private => $privkey); 367 $self->txn (invert_private_key => private => $privkey);
333}; 368});
334 369
335=item $txn = $fcp->txn_get_size ($uri) 370=item $txn = $fcp->txn_get_size ($uri)
336 371
337=item $length = $fcp->get_size ($uri) 372=item $length = $fcp->get_size ($uri)
338 373
341 376
342UNTESTED. 377UNTESTED.
343 378
344=cut 379=cut
345 380
346_txn get_size => sub { 381$txn->(get_size => sub {
347 my ($self, $uri) = @_; 382 my ($self, $uri) = @_;
348 383
349 $self->txn (get_size => URI => $uri); 384 $self->txn (get_size => URI => $uri);
350}; 385});
351 386
352=item $txn = $fcp->txn_client_get ($uri [, $htl = 15 [, $removelocal = 0]]) 387=item $txn = $fcp->txn_client_get ($uri [, $htl = 15 [, $removelocal = 0]])
353 388
354=item ($metadata, $data) = @{ $fcp->client_get ($uri, $htl, $removelocal) 389=item ($metadata, $data) = @{ $fcp->client_get ($uri, $htl, $removelocal)
355 390
365 ) 400 )
366 }; 401 };
367 402
368=cut 403=cut
369 404
370_txn client_get => sub { 405$txn->(client_get => sub {
371 my ($self, $uri, $htl, $removelocal) = @_; 406 my ($self, $uri, $htl, $removelocal) = @_;
372 407
373 $self->txn (client_get => URI => $uri, hops_to_live => ($htl || 15), remove_local_key => $removelocal ? "true" : "false"); 408 $self->txn (client_get => URI => $uri, hops_to_live => (defined $htl ? $htl :15),
409 remove_local_key => $removelocal ? "true" : "false");
374}; 410});
375 411
412=item $txn = $fcp->txn_client_put ($uri, $metadata, $data, $htl, $removelocal)
413
414=item my $uri = $fcp->client_put ($uri, $metadata, $data, $htl, $removelocal);
415
416Insert a new key. If the client is inserting a CHK, the URI may be
417abbreviated as just CHK@. In this case, the node will calculate the
418CHK.
419
420C<$meta> can be a reference or a string (ONLY THE STRING CASE IS IMPLEMENTED!).
421
422THIS INTERFACE IS UNTESTED AND SUBJECT TO CHANGE.
423
424=cut
425
426$txn->(client_put => sub {
427 my ($self, $uri, $meta, $data, $htl, $removelocal) = @_;
428
429 $self->txn (client_put => URI => $uri, hops_to_live => (defined $htl ? $htl :15),
430 remove_local_key => $removelocal ? "true" : "false",
431 data => "$meta$data", metadata_length => length $meta);
432});
433
434} # transactions
435
376=item MISSING: ClientPut 436=item MISSING: (ClientPut), InsretKey
377 437
378=back 438=back
379 439
380=head2 THE Net::FCP::Txn CLASS 440=head2 THE Net::FCP::Txn CLASS
381 441
483 543
484sub userdata($$) { 544sub userdata($$) {
485 my ($self, $data) = @_; 545 my ($self, $data) = @_;
486 $self->{userdata} = $data; 546 $self->{userdata} = $data;
487 $self; 547 $self;
548}
549
550=item $txn->cancel (%attr)
551
552Cancels the operation with a C<cancel> exception anf the given attributes
553(consider at least giving the attribute C<reason>).
554
555UNTESTED.
556
557=cut
558
559sub cancel {
560 my ($self, %attr) = @_;
561 $self->throw (Net::FCP::Exception->new (cancel => { %attr }));
562 $self->set_result;
563 $self->eof;
488} 564}
489 565
490sub fh_ready_w { 566sub fh_ready_w {
491 my ($self) = @_; 567 my ($self) = @_;
492 568
532 } else { 608 } else {
533 $self->eof; 609 $self->eof;
534 } 610 }
535} 611}
536 612
537sub rcv_data {
538 my ($self, $chunk) = @_;
539
540 $self->{data} .= $chunk;
541
542 $self->progress ("data", { chunk => length $chunk, total => length $self->{data}, end => $self->{datalength} });
543}
544
545sub rcv { 613sub rcv {
546 my ($self, $type, $attr) = @_; 614 my ($self, $type, $attr) = @_;
547 615
548 $type = Net::FCP::tolc $type; 616 $type = Net::FCP::tolc $type;
549 617
557} 625}
558 626
559# used as a default exception thrower 627# used as a default exception thrower
560sub rcv_throw_exception { 628sub rcv_throw_exception {
561 my ($self, $attr, $type) = @_; 629 my ($self, $attr, $type) = @_;
562 $self->throw (new Net::FCP::Exception $type, $attr); 630 $self->throw (Net::FCP::Exception->new ($type, $attr));
563} 631}
564 632
565*rcv_failed = \&Net::FCP::Txn::rcv_throw_exception; 633*rcv_failed = \&Net::FCP::Txn::rcv_throw_exception;
566*rcv_format_error = \&Net::FCP::Txn::rcv_throw_exception; 634*rcv_format_error = \&Net::FCP::Txn::rcv_throw_exception;
567 635
568sub throw { 636sub throw {
569 my ($self, $exc) = @_; 637 my ($self, $exc) = @_;
570 638
571 $self->{exception} = $exc; 639 $self->{exception} = $exc;
572 $self->set_result (1); 640 $self->set_result;
573 $self->eof; # must be last to avoid loops 641 $self->eof; # must be last to avoid loops
574} 642}
575 643
576sub set_result { 644sub set_result {
577 my ($self, $result) = @_; 645 my ($self, $result) = @_;
589 delete $self->{w}; 657 delete $self->{w};
590 delete $self->{fh}; 658 delete $self->{fh};
591 659
592 delete $self->{fcp}{txn}{$self}; 660 delete $self->{fcp}{txn}{$self};
593 661
594 $self->set_result; # just in case 662 unless (exists $self->{result}) {
663 $self->throw (Net::FCP::Exception->new (short_data => {
664 reason => "unexpected eof or internal node error",
665 }));
666 }
595} 667}
596 668
597sub progress { 669sub progress {
598 my ($self, $type, $attr) = @_; 670 my ($self, $type, $attr) = @_;
599 $self->{fcp}->progress ($self, $type, $attr); 671 $self->{fcp}->progress ($self, $type, $attr);
652 724
653use base Net::FCP::Txn; 725use base Net::FCP::Txn;
654 726
655sub rcv_success { 727sub rcv_success {
656 my ($self, $attr) = @_; 728 my ($self, $attr) = @_;
657
658 $self->set_result ([$attr->{PublicKey}, $attr->{PrivateKey}]); 729 $self->set_result ([$attr->{PublicKey}, $attr->{PrivateKey}]);
659} 730}
660 731
661package Net::FCP::Txn::InvertPrivateKey; 732package Net::FCP::Txn::InsertPrivateKey;
662 733
663use base Net::FCP::Txn; 734use base Net::FCP::Txn;
664 735
665sub rcv_success { 736sub rcv_success {
666 my ($self, $attr) = @_; 737 my ($self, $attr) = @_;
667
668 $self->set_result ($attr->{PublicKey}); 738 $self->set_result ($attr->{PublicKey});
669} 739}
670 740
671package Net::FCP::Txn::GetSize; 741package Net::FCP::Txn::GetSize;
672 742
673use base Net::FCP::Txn; 743use base Net::FCP::Txn;
674 744
675sub rcv_success { 745sub rcv_success {
676 my ($self, $attr) = @_; 746 my ($self, $attr) = @_;
677
678 $self->set_result ($attr->{Length}); 747 $self->set_result ($attr->{Length});
679} 748}
680 749
681package Net::FCP::Txn::GetPut; 750package Net::FCP::Txn::GetPut;
682 751
701 770
702use base Net::FCP::Txn::GetPut; 771use base Net::FCP::Txn::GetPut;
703 772
704*rcv_data_not_found = \&Net::FCP::Txn::rcv_throw_exception; 773*rcv_data_not_found = \&Net::FCP::Txn::rcv_throw_exception;
705 774
706sub rcv_data_found { 775sub rcv_data {
707 my ($self, $attr, $type) = @_;
708
709 $self->progress ($type, $attr);
710
711 $self->{datalength} = hex $attr->{data_length};
712 $self->{metalength} = hex $attr->{metadata_length};
713}
714
715sub eof {
716 my ($self) = @_; 776 my ($self, $chunk) = @_;
777
778 $self->{data} .= $chunk;
779
780 $self->progress ("data", { chunk => length $chunk, received => length $self->{data}, total => $self->{datalength} });
717 781
718 if ($self->{datalength} == length $self->{data}) { 782 if ($self->{datalength} == length $self->{data}) {
719 my $data = delete $self->{data}; 783 my $data = delete $self->{data};
720 my $meta = Net::FCP::parse_metadata substr $data, 0, $self->{metalength}, ""; 784 my $meta = Net::FCP::parse_metadata substr $data, 0, $self->{metalength}, "";
721 785
722 $self->set_result ([$meta, $data]); 786 $self->set_result ([$meta, $data]);
723 } elsif (!exists $self->{result}) {
724 $self->throw (Net::FCP::Exception->new (short_data => {
725 reason => "unexpected eof or internal node error",
726 received => length $self->{data},
727 expected => $self->{datalength},
728 }));
729 } 787 }
788}
789
790sub rcv_data_found {
791 my ($self, $attr, $type) = @_;
792
793 $self->progress ($type, $attr);
794
795 $self->{datalength} = hex $attr->{data_length};
796 $self->{metalength} = hex $attr->{metadata_length};
730} 797}
731 798
732package Net::FCP::Txn::ClientPut; 799package Net::FCP::Txn::ClientPut;
733 800
734use base Net::FCP::Txn::GetPut; 801use base Net::FCP::Txn::GetPut;
743 810
744sub rcv_success { 811sub rcv_success {
745 my ($self, $attr, $type) = @_; 812 my ($self, $attr, $type) = @_;
746 $self->set_result ($attr); 813 $self->set_result ($attr);
747} 814}
815
816=back
817
818=head2 The Net::FCP::Exception CLASS
819
820Any unexpected (non-standard) responses that make it impossible to return
821the advertised result will result in an exception being thrown when the
822C<result> method is called.
823
824These exceptions are represented by objects of this class.
825
826=over 4
827
828=cut
748 829
749package Net::FCP::Exception; 830package Net::FCP::Exception;
750 831
751use overload 832use overload
752 '""' => sub { 833 '""' => sub {
753 "Net::FCP::Exception<<$_[0][0]," . (join ":", %{$_[0][1]}) . ">>\n"; 834 "Net::FCP::Exception<<$_[0][0]," . (join ":", %{$_[0][1]}) . ">>\n";
754 }; 835 };
755 836
837=item $exc = new Net::FCP::Exception $type, \%attr
838
839Create a new exception object of the given type (a string like
840C<route_not_found>), and a hashref containing additional attributes
841(usually the attributes of the message causing the exception).
842
843=cut
844
756sub new { 845sub new {
757 my ($class, $type, $attr) = @_; 846 my ($class, $type, $attr) = @_;
758 847
759 bless [Net::FCP::tolc $type, { %$attr }], $class; 848 bless [Net::FCP::tolc $type, { %$attr }], $class;
760} 849}
761 850
851=item $exc->type([$type])
852
853With no arguments, returns the exception type. Otherwise a boolean
854indicating wether the exception is of the given type is returned.
855
856=cut
857
858sub type {
859 my ($self, $type) = @_;
860
861 @_ >= 2
862 ? $self->[0] eq $type
863 : $self->[0];
864}
865
866=item $exc->attr([$attr])
867
868With no arguments, returns the attributes. Otherwise the named attribute
869value is returned.
870
871=cut
872
873sub attr {
874 my ($self, $attr) = @_;
875
876 @_ >= 2
877 ? $self->[1]{$attr}
878 : $self->[1];
879}
880
762=back 881=back
763 882
764=head1 SEE ALSO 883=head1 SEE ALSO
765 884
766L<http://freenet.sf.net>. 885L<http://freenet.sf.net>.
772 Marc Lehmann <pcg@goof.com> 891 Marc Lehmann <pcg@goof.com>
773 http://www.goof.com/pcg/marc/ 892 http://www.goof.com/pcg/marc/
774 893
775=cut 894=cut
776 895
896package Net::FCP::Event::Auto;
897
898my @models = (
899 [Coro => Coro::Event:: ],
900 [Event => Event::],
901 [Glib => Glib:: ],
902 [Tk => Tk::],
903);
904
905sub AUTOLOAD {
906 $AUTOLOAD =~ s/.*://;
907
908 for (@models) {
909 my ($model, $package) = @$_;
910 if (defined ${"$package\::VERSION"}) {
911 $EVENT = "Net::FCP::Event::$model";
912 eval "require $EVENT"; die if $@;
913 goto &{"$EVENT\::$AUTOLOAD"};
914 }
915 }
916
917 for (@models) {
918 my ($model, $package) = @$_;
919 $EVENT = "Net::FCP::Event::$model";
920 if (eval "require $EVENT") {
921 goto &{"$EVENT\::$AUTOLOAD"};
922 }
923 }
924
925 die "No event module selected for Net::FCP and autodetect failed. Install any of these: Coro, Event, Glib or Tk.";
926}
927
7771; 9281;
778 929

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines