source: main/trunk/greenstone2/perllib/plugins/EncodingUtil.pm@ 31491

Last change on this file since 31491 was 31491, checked in by kjdon, 4 years ago

need to normalize the name when we look up in hte block hash too, for macos

File size: 21.0 KB
Line 
1###########################################################################
2#
3# EncodingUtil.pm -- base class for file and directory plugins - aims to
4# handle all encoding stuff, to keep it in one place
5# A component of the Greenstone digital library software
6# from the New Zealand Digital Library Project at the
7# University of Waikato, New Zealand.
8#
9# Copyright (C) 2017 New Zealand Digital Library Project
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24#
25###########################################################################
26
27package EncodingUtil;
28
29use strict;
30no strict 'subs';
31no strict 'refs'; # allow filehandles to be variables and viceversa
32
33use encodings;
34use Unicode::Normalize 'normalize';
35
36use PrintInfo;
37use Encode;
38use Unicode::Normalize 'normalize';
39
40BEGIN {
41 @EncodingUtil::ISA = ( 'PrintInfo' );
42}
43
44our $encoding_list =
45 [ { 'name' => "ascii",
46 'desc' => "{BasePlugin.encoding.ascii}" },
47 { 'name' => "utf8",
48 'desc' => "{BasePlugin.encoding.utf8}" },
49 { 'name' => "unicode",
50 'desc' => "{BasePlugin.encoding.unicode}" } ];
51
52
53my $e = $encodings::encodings;
54foreach my $enc (sort {$e->{$a}->{'name'} cmp $e->{$b}->{'name'}} keys (%$e))
55{
56 my $hashEncode =
57 {'name' => $enc,
58 'desc' => $e->{$enc}->{'name'}};
59
60 push(@{$encoding_list},$hashEncode);
61}
62
63our $encoding_plus_auto_list =
64 [ { 'name' => "auto",
65 'desc' => "{BasePlugin.filename_encoding.auto}" },
66 { 'name' => "auto-language-analysis",
67 'desc' => "{BasePlugin.filename_encoding.auto_language_analysis}" }, # textcat
68 { 'name' => "auto-filesystem-encoding",
69 'desc' => "{BasePlugin.filename_encoding.auto_filesystem_encoding}" }, # locale
70 { 'name' => "auto-fl",
71 'desc' => "{BasePlugin.filename_encoding.auto_fl}" }, # locale followed by textcat
72 { 'name' => "auto-lf",
73 'desc' => "{BasePlugin.filename_encoding.auto_lf}" } ]; # texcat followed by locale
74
75push(@{$encoding_plus_auto_list},@{$encoding_list});
76
77my $arguments =
78 [ { 'name' => "block_exp",
79 'desc' => "{BasePlugin.block_exp}",
80 'type' => "regexp",
81 'deft' => "",
82 'reqd' => "no" },
83 { 'name' => "no_blocking",
84 'desc' => "{BasePlugin.no_blocking}",
85 'type' => "flag",
86 'reqd' => "no"},
87 { 'name' => "filename_encoding",
88 'desc' => "{BasePlugin.filename_encoding}",
89 'type' => "enum",
90 'deft' => "auto",
91 'list' => $encoding_plus_auto_list,
92 'reqd' => "no" }
93 ];
94
95my $options = { 'name' => "EncodingUtil",
96 'desc' => "{EncodingUtil.desc}",
97 'abstract' => "yes",
98 'inherits' => "no",
99 'args' => $arguments };
100
101
102sub new {
103
104 my ($class) = shift (@_);
105 my ($pluginlist,$inputargs,$hashArgOptLists,$auxiliary) = @_;
106 push(@$pluginlist, $class);
107
108 push(@{$hashArgOptLists->{"ArgList"}},@{$arguments});
109 push(@{$hashArgOptLists->{"OptList"}},$options);
110
111 my $self = new PrintInfo($pluginlist, $inputargs, $hashArgOptLists,$auxiliary);
112
113 return bless $self, $class;
114
115}
116
117sub init {
118 my $self = shift (@_);
119 my ($verbosity, $outhandle, $failhandle) = @_;
120
121 print STDERR "guess encoding = ".$self->guess_filesystem_encoding()."\n";
122 print STDERR "get encoding = ".$self->get_filesystem_encoding()."\n";
123
124 # verbosity is passed through from the processor
125 $self->{'verbosity'} = $verbosity;
126
127 # as are the outhandle and failhandle
128 $self->{'outhandle'} = $outhandle if defined $outhandle;
129 $self->{'failhandle'} = $failhandle;
130
131}
132
133# converts raw filesystem filename to perl unicode format
134sub raw_filename_to_unicode {
135 my $self = shift (@_);
136 my ($file) = @_;
137
138 my $unicode_file = "";
139 ### need it in perl unicode, not raw filesystem
140 my $filename_encoding = $self->guess_filesystem_encoding();
141
142 # copied this from set_Source_metadata in BasePlugin
143 if ((defined $filename_encoding) && ($filename_encoding ne "ascii")) {
144 # Use filename_encoding to map raw filename to a Perl unicode-aware string
145 $unicode_file = decode($filename_encoding,$file);
146 }
147 else {
148 # otherwise generate %xx encoded version of filename for char > 127
149 $unicode_file = &unicode::raw_filename_to_url_encoded($file);
150 }
151 return $unicode_file;
152
153}
154# just converts path as is to utf8.
155sub filepath_to_utf8 {
156 my $self = shift (@_);
157 my ($file, $file_encoding) = @_;
158 my $filemeta = $file;
159
160 my $filename_encoding = $self->{'filename_encoding'}; # filename encoding setting
161
162 # Whenever filename-encoding is set to any of the auto settings, we
163 # check if the filename is already in UTF8. If it is, then we're done.
164 if($filename_encoding =~ m/auto/) {
165 if(&unicode::check_is_utf8($filemeta))
166 {
167 $filename_encoding = "utf8";
168 return $filemeta;
169 }
170 }
171
172 # Auto setting, but filename is not utf8
173 if ($filename_encoding eq "auto")
174 {
175 # try textcat
176 $filename_encoding = $self->textcat_encoding($filemeta);
177
178 # check the locale next
179 $filename_encoding = $self->locale_encoding() if $filename_encoding eq "undefined";
180
181
182 # now try the encoding of the document, if available
183 if ($filename_encoding eq "undefined" && defined $file_encoding) {
184 $filename_encoding = $file_encoding;
185 }
186
187 }
188
189 elsif ($filename_encoding eq "auto-language-analysis")
190 {
191 $filename_encoding = $self->textcat_encoding($filemeta);
192
193 # now try the encoding of the document, if available
194 if ($filename_encoding eq "undefined" && defined $file_encoding) {
195 $filename_encoding = $file_encoding;
196 }
197 }
198
199 elsif ($filename_encoding eq "auto-filesystem-encoding")
200 {
201 # try locale
202 $filename_encoding = $self->locale_encoding();
203 }
204
205 elsif ($filename_encoding eq "auto-fl")
206 {
207 # filesystem-encoding (locale) then language-analysis (textcat)
208 $filename_encoding = $self->locale_encoding();
209
210 # try textcat
211 $filename_encoding = $self->textcat_encoding($filemeta) if $filename_encoding eq "undefined";
212
213 # else assume filename encoding is encoding of file content, if that's available
214 if ($filename_encoding eq "undefined" && defined $file_encoding) {
215 $filename_encoding = $file_encoding;
216 }
217 }
218
219 elsif ($filename_encoding eq "auto-lf")
220 {
221 # language-analysis (textcat) then filesystem-encoding (locale)
222 $filename_encoding = $self->textcat_encoding($filemeta);
223
224 # guess filename encoding from encoding of file content, if available
225 if ($filename_encoding eq "undefined" && defined $file_encoding) {
226 $filename_encoding = $file_encoding;
227 }
228
229 # try locale
230 $filename_encoding = $self->locale_encoding() if $filename_encoding eq "undefined";
231 }
232
233 # if still undefined, use utf8 as fallback
234 if ($filename_encoding eq "undefined") {
235 $filename_encoding = "utf8";
236 }
237
238 #print STDERR "**** UTF8 encoding the filename $filemeta ";
239
240 # if the filename encoding is set to utf8 but it isn't utf8 already--such as when
241 # 1. the utf8 fallback is used, or 2. if the system locale is used and happens to
242 # be always utf8 (in which case the filename's encoding is also set as utf8 even
243 # though the filename need not be if it originates from another system)--in such
244 # cases attempt to make the filename utf8 to match.
245 if($filename_encoding eq "utf8" && !&unicode::check_is_utf8($filemeta)) {
246 &unicode::ensure_utf8(\$filemeta);
247 }
248
249 # convert non-unicode encodings to utf8
250 if ($filename_encoding !~ m/(?:ascii|utf8|unicode)/) {
251 $filemeta = &unicode::unicode2utf8(
252 &unicode::convert2unicode($filename_encoding, \$filemeta)
253 );
254 }
255
256 #print STDERR " from encoding $filename_encoding -> $filemeta\n";
257 return $filemeta;
258}
259
260# gets the filename with no path, converts to utf8, and then dm safes it.
261# filename_encoding set by user
262sub filename_to_utf8_metadata
263{
264 my $self = shift (@_);
265 my ($file, $file_encoding) = @_;
266
267 my $outhandle = $self->{'outhandle'};
268
269 print $outhandle "****!!!!**** BasePlugin::filename_to_utf8_metadata now deprecated\n";
270 my ($cpackage,$cfilename,$cline,$csubr,$chas_args,$cwantarray) = caller(0);
271 print $outhandle "Calling method: $cfilename:$cline $cpackage->$csubr\n";
272
273 my ($filemeta) = $file =~ /([^\\\/]+)$/; # getting the tail of the filepath (skips all string parts containing slashes upto the end)
274 $filemeta = $self->filepath_to_utf8($filemeta, $file_encoding);
275
276 return $filemeta;
277}
278
279sub locale_encoding {
280 my $self = shift(@_);
281
282 if (!defined $self->{'filesystem_encoding'}) {
283 $self->{'filesystem_encoding'} = $self->get_filesystem_encoding();
284 }
285
286 #print STDERR "*** filename encoding determined based on locale: " . $self->{'filesystem_encoding'} . "\n";
287 return $self->{'filesystem_encoding'}; # can be the string "undefined"
288}
289
290
291sub textcat_encoding {
292 my $self = shift(@_);
293 my ($filemeta) = @_;
294
295 # analyse filenames without extensions and digits (and trimmed of
296 # surrounding whitespace), so that irrelevant chars don't confuse
297 # textcat
298 my $strictfilemeta = $filemeta;
299 $strictfilemeta =~ s/\.[^\.]+$//g;
300 $strictfilemeta =~ s/\d//g;
301 $strictfilemeta =~ s/^\s*//g;
302 $strictfilemeta =~ s/\s*$//g;
303
304 my $filename_encoding = $self->encoding_from_language_analysis($strictfilemeta);
305 if(!defined $filename_encoding) {
306 $filename_encoding = "undefined";
307 }
308
309 return $filename_encoding; # can be the string "undefined"
310}
311
312# performs textcat
313sub encoding_from_language_analysis {
314 my $self = shift(@_);
315 my ($text) = @_;
316
317 my $outhandle = $self->{'outhandle'};
318 my $best_encoding = undef;
319
320 # get the language/encoding of the textstring using textcat
321 require textcat; # Only load the textcat module if it is required
322 $self->{'textcat'} = new textcat() unless defined($self->{'textcat'});
323 my $results = $self->{'textcat'}->classify_cached_filename(\$text);
324
325
326 if (scalar @$results < 0) {
327 return undef;
328 }
329
330 # We have some results, we choose the first
331 my ($language, $encoding) = $results->[0] =~ /^([^-]*)(?:-(.*))?$/;
332
333 $best_encoding = $encoding;
334 if (!defined $best_encoding) {
335 return undef;
336 }
337
338 if (defined $best_encoding && $best_encoding =~ m/^iso_8859/ && &unicode::check_is_utf8($text)) {
339 # the text is valid utf8, so assume that's the real encoding (since textcat is based on probabilities)
340 $best_encoding = 'utf8';
341 }
342
343
344 # check for equivalents where textcat doesn't have some encodings...
345 # eg MS versions of standard encodings
346 if (defined $best_encoding && $best_encoding =~ /^iso_8859_(\d+)/) {
347 my $iso = $1; # which variant of the iso standard?
348 # iso-8859 sets don't use chars 0x80-0x9f, windows codepages do
349 if ($text =~ /[\x80-\x9f]/) {
350 # Western Europe
351 if ($iso == 1 or $iso == 15) { $best_encoding = 'windows_1252' }
352 elsif ($iso == 2) {$best_encoding = 'windows_1250'} # Central Europe
353 elsif ($iso == 5) {$best_encoding = 'windows_1251'} # Cyrillic
354 elsif ($iso == 6) {$best_encoding = 'windows_1256'} # Arabic
355 elsif ($iso == 7) {$best_encoding = 'windows_1253'} # Greek
356 elsif ($iso == 8) {$best_encoding = 'windows_1255'} # Hebrew
357 elsif ($iso == 9) {$best_encoding = 'windows_1254'} # Turkish
358 }
359 }
360
361 if (defined $best_encoding && $best_encoding !~ /^(ascii|utf8|unicode)$/ &&
362 !defined $encodings::encodings->{$best_encoding})
363 {
364 if ($self->{'verbosity'}) {
365 gsprintf($outhandle, "BasePlugin: {ReadTextFile.unsupported_encoding}\n", $text, $best_encoding, "undef");
366 }
367 $best_encoding = undef;
368 }
369
370 return $best_encoding;
371}
372
373
374
375sub deduce_filename_encoding
376{
377 my $self = shift (@_);
378 my ($file,$metadata,$plugin_filename_encoding) = @_;
379
380 my $gs_filename_encoding = $metadata->{"gs.filenameEncoding"};
381 my $deduced_filename_encoding = undef;
382
383 # Start by looking for manually assigned metadata
384 if (defined $gs_filename_encoding) {
385 if (ref ($gs_filename_encoding) eq "ARRAY") {
386 my $outhandle = $self->{'outhandle'};
387
388 $deduced_filename_encoding = $gs_filename_encoding->[0];
389
390 my $num_vals = scalar(@$gs_filename_encoding);
391 if ($num_vals>1) {
392 print $outhandle "Warning: gs.filenameEncoding multiply defined for $file\n";
393 print $outhandle " Selecting first value: $deduced_filename_encoding\n";
394 }
395 }
396 else {
397 $deduced_filename_encoding = $gs_filename_encoding;
398 }
399 }
400
401 if (!defined $deduced_filename_encoding || ($deduced_filename_encoding =~ m/^\s*$/)) {
402 # Look to see if plugin specifies this value
403
404 if (defined $plugin_filename_encoding) {
405 # First look to see if we're using any of the "older" (i.e. deprecated auto-... plugin options)
406 if ($plugin_filename_encoding =~ m/^auto-.*$/) {
407 my $outhandle = $self->{'outhandle'};
408 print $outhandle "Warning: $plugin_filename_encoding is no longer supported\n";
409 print $outhandle " default to 'auto'\n";
410 $self->{'filename_encoding'} = $plugin_filename_encoding = "auto";
411 }
412
413 if ($plugin_filename_encoding ne "auto") {
414 # We've been given a specific filenamne encoding
415 # => so use it!
416 $deduced_filename_encoding = $plugin_filename_encoding;
417 }
418 }
419 }
420
421 if (!defined $deduced_filename_encoding || ($deduced_filename_encoding =~ m/^\s*$/)) {
422
423 # Look to file system to provide a character encoding
424
425 # If Windows NTFS, then -- assuming we work with long file names got through
426 # Win32::GetLongFilePath() -- then the underlying file system is UTF16
427
428 if (($ENV{'GSDLOS'} =~ m/^windows$/i) && ($^O ne "cygwin")) {
429 # Can do better than working with the DOS character encoding returned by locale
430 $deduced_filename_encoding = "unicode";
431 }
432 else {
433 # Unix of some form or other
434
435 # See if we can determine the file system encoding through locale
436 $deduced_filename_encoding = $self->locale_encoding();
437
438 # if locale shows us filesystem is utf8, check to see filename is consistent
439 # => if not, then we have an "alien" filename on our hands
440
441 if (defined $deduced_filename_encoding && $deduced_filename_encoding =~ m/^utf-?8$/i) {
442 if (!&unicode::check_is_utf8($file)) {
443 # "alien" filename, so revert
444 $deduced_filename_encoding = undef;
445 }
446 }
447 }
448 }
449
450# if (!defined $deduced_filename_encoding || ($deduced_filename_encoding =~ m/^\s*$/)) {
451# # Last chance, apply textcat to deduce filename encoding
452# $deduced_filename_encoding = $self->textcat_encoding($file);
453# }
454
455 if ($self->{'verbosity'}>3) {
456 my $outhandle = $self->{'outhandle'};
457
458 if (defined $deduced_filename_encoding) {
459 print $outhandle " Deduced filename encoding as: $deduced_filename_encoding\n";
460 }
461 else {
462 print $outhandle " No filename encoding deduced\n";
463 }
464 }
465
466 return $deduced_filename_encoding;
467}
468
469
470sub guess_filesystem_encoding
471{
472 my $self = shift (@_);
473 # Look to file system to provide a character encoding
474 my $deduced_filename_encoding = "";
475 # If Windows NTFS, then -- assuming we work with long file names got through
476 # Win32::GetLongFilePath() -- then the underlying file system is UTF16
477
478 if (($ENV{'GSDLOS'} =~ m/^windows$/i) && ($^O ne "cygwin")) {
479 # Can do better than working with the DOS character encoding returned by locale
480 $deduced_filename_encoding = "unicode";
481 }
482 else {
483 # Unix of some form or other
484
485 # See if we can determine the file system encoding through locale
486 $deduced_filename_encoding = $self->locale_encoding(); #utf8??
487
488 }
489 print STDERR "guessing filesystem encoding is $deduced_filename_encoding\n";
490 return $deduced_filename_encoding;
491}
492
493
494# uses locale
495sub get_filesystem_encoding
496{
497
498 my $self = shift(@_);
499
500 my $outhandle = $self->{'outhandle'};
501 my $filesystem_encoding = undef;
502
503 eval {
504 # Works for Windows as well, returning the DOS code page in use
505 use POSIX qw(locale_h);
506
507 # With only one parameter, setlocale retrieves the
508 # current value
509 my $current_locale = setlocale(LC_CTYPE);
510
511 my $char_encoding = undef;
512 if ($current_locale =~ m/\./) {
513 ($char_encoding) = ($current_locale =~ m/^.*\.(.*?)$/);
514 $char_encoding = lc($char_encoding);
515 }
516 else {
517 if ($current_locale =~ m/^(posix|c)$/i) {
518 $char_encoding = "ascii";
519 }
520 }
521
522 if (defined $char_encoding) {
523 if ($char_encoding =~ m/^(iso)(8859)-?(\d{1,2})$/) {
524 $char_encoding = "$1\_$2\_$3";
525 }
526
527 $char_encoding =~ s/-/_/g;
528 $char_encoding =~ s/^utf_8$/utf8/;
529
530 if ($char_encoding =~ m/^\d+$/) {
531 if (defined $encodings::encodings->{"windows_$char_encoding"}) {
532 $char_encoding = "windows_$char_encoding";
533 }
534 elsif (defined $encodings::encodings->{"dos_$char_encoding"}) {
535 $char_encoding = "dos_$char_encoding";
536 }
537 }
538
539 if (($char_encoding =~ m/(?:ascii|utf8|unicode)/)
540 || (defined $encodings::encodings->{$char_encoding})) {
541 $filesystem_encoding = $char_encoding;
542 }
543 else {
544 print $outhandle "Warning: Unsupported character encoding '$char_encoding' from locale '$current_locale'\n";
545 }
546 }
547
548
549 };
550 if ($@) {
551 print $outhandle "$@\n";
552 print $outhandle "Warning: Unable to establish locale. Will assume filesystem is UTF-8\n";
553
554 }
555
556 return $filesystem_encoding;
557}
558
559
560
561# write_file -- used by ConvertToPlug, for example in post processing
562#
563# where should this go, is here the best place??
564sub utf8_write_file {
565 my $self = shift (@_);
566 my ($textref, $filename) = @_;
567
568 if (!open (FILE, ">:utf8", $filename)) {
569 gsprintf(STDERR, "ConvertToPlug::write_file {ConvertToPlug.could_not_open_for_writing} ($!)\n", $filename);
570 die "\n";
571 }
572 print FILE $$textref;
573
574 close FILE;
575}
576
577sub block_raw_filename {
578
579 my $self = shift (@_);
580 my ($block_hash,$filename_full_path) = @_;
581
582 my $unicode_filename = $self->raw_filename_to_unicode($filename_full_path);
583 return $self->block_filename($block_hash, $unicode_filename);
584}
585
586# block unicode string filename
587sub block_filename
588{
589 my $self = shift (@_);
590 my ($block_hash,$filename_full_path) = @_;
591 print STDERR "in block filename $filename_full_path\n";
592 print STDERR &unicode::debug_unicode_string($filename_full_path)."\n";
593
594 if (($ENV{'GSDLOS'} =~ m/^windows$/) && ($^O ne "cygwin")) {
595 # block hash contains long names, lets make sure that we were passed a long name
596 $filename_full_path = &util::upgrade_if_dos_filename($filename_full_path);
597 # lower case the entire thing, eg for cover.jpg when its actually cover.JPG
598 my $lower_filename_full_path = lc($filename_full_path);
599 $block_hash->{'file_blocks'}->{$lower_filename_full_path} = 1;
600
601 }
602 elsif ($ENV{'GSDLOS'} =~ m/^darwin$/) {
603 # we need to normalize the filenames
604 my $composed_filename_full_path = normalize('C', $filename_full_path);
605 print STDERR "darwin, composed filename =". &unicode::debug_unicode_string($composed_filename_full_path)."\n";
606 $block_hash->{'file_blocks'}->{$composed_filename_full_path} = 1;
607 }
608
609 else {
610 $block_hash->{'file_blocks'}->{$filename_full_path} = 1;
611 }
612}
613
614
615# filename is raw filesystem name
616sub raw_file_is_blocked {
617 my $self = shift (@_);
618 my ($block_hash, $filename_full_path) = @_;
619
620 my $unicode_filename_full_path = $self->raw_filename_to_unicode($filename_full_path);
621 return $self->file_is_blocked($block_hash, $unicode_filename_full_path);
622}
623
624# filename must be perl unicode string
625sub file_is_blocked {
626 my $self = shift (@_);
627 my ($block_hash, $filename_full_path) = @_;
628
629 #
630 print STDERR "in file is blocked $filename_full_path\n";
631 print STDERR &unicode::debug_unicode_string($filename_full_path)."\n";
632 if (($ENV{'GSDLOS'} =~ m/^windows$/) && ($^O ne "cygwin")) {
633 # convert to long filenames if needed
634 $filename_full_path = &util::upgrade_if_dos_filename($filename_full_path);
635 # all block paths are lowercased.
636 my $lower_filename = lc ($filename_full_path);
637 if (defined $block_hash->{'file_blocks'}->{$lower_filename}) {
638 $self->{'num_blocked'} ++;
639 return 1;
640 }
641 }
642 elsif ($ENV{'GSDLOS'} =~ m/^darwin$/) {
643
644 # on mac, we want composed form in the block hash
645 my $composed_form = normalize('C', $filename_full_path);
646 print STDERR "gsdlos = darwin, composed = ". &unicode::debug_unicode_string($composed_form) ."\n";
647 if (defined $block_hash->{'file_blocks'}->{$composed_form}) {
648 $self->{'num_blocked'} ++;
649 print STDERR "BLOCKED 1\n";
650 return 1;
651 }
652 }
653
654 else {
655 if (defined $block_hash->{'file_blocks'}->{$filename_full_path}) {
656 $self->{'num_blocked'} ++;
657 print STDERR "BLOCKED\n";
658 return 1;
659 }
660 }
661 # check Directory plugin's own block_exp
662 if ($self->{'block_exp'} ne "" && $filename_full_path =~ /$self->{'block_exp'}/) {
663 $self->{'num_blocked'} ++;
664 return 1; # blocked
665 }
666 print STDERR "NOT BLOCKED\n";
667 return 0;
668}
669
670
6711;
672
Note: See TracBrowser for help on using the repository browser.