add cmd ratelimits, restore regex is_ipaddr
[spider.git] / perl / DXUtil.pm
1 #
2 # various utilities which are exported globally
3 #
4 # Copyright (c) 1998 - Dirk Koopman G1TLH
5 #
6 #
7 #
8
9 package DXUtil;
10
11
12 use Date::Parse;
13 use IO::File;
14 use File::Copy;
15 use Data::Dumper;
16 use Time::HiRes qw(gettimeofday tv_interval);
17 use Text::Wrap;
18 use strict;
19
20 use vars qw(@month %patmap $pi $d2r $r2d @ISA @EXPORT);
21
22 require Exporter;
23 @ISA = qw(Exporter);
24 @EXPORT = qw(atime ztime cldate cldatetime slat slong yesno promptf 
25                          parray parraypairs phex phash shellregex readfilestr writefilestr
26                          filecopy ptimelist
27              print_all_fields cltounix unpad is_callsign is_latlong
28                          is_qra is_freq is_digits is_pctext is_pcflag insertitem deleteitem
29                          is_prefix dd is_ipaddr $pi $d2r $r2d localdata localdata_mv
30                          diffms _diffms _diffus difft parraydifft is_ztime basecall
31                          normalise_call is_numeric
32             );
33
34
35 @month = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
36 %patmap = (
37                    '*' => '.*',
38                    '?' => '.',
39                    '[' => '[',
40                    ']' => ']'
41 );
42
43 $pi = 3.141592653589;
44 $d2r = ($pi/180);
45 $r2d = (180/$pi);
46
47
48 # BEGIN {
49 #       our $enable_ptonok = 0;
50 #       our $ptonok;
51
52
53 #       if ($enable_ptonok && !$main::is_win) {
54 #               eval {require Socket; Socket->import(qw(AF_INET6 AF_INET inet_pton)); };
55 #               unless ($@) {
56 #                       $ptonok = !defined inet_pton(AF_INET,  '016.17.184.1')
57 #                               && !defined inet_pton(AF_INET6, '2067::1:')
58 #                               # Some old versions of Socket are hopelessly broken
59 #                               && length(inet_pton(AF_INET, '1.1.1.1')) == 4;
60 #               }
61 #       }
62 # }
63
64
65
66 # a full time for logging and other purposes
67 sub atime
68 {
69         my $t = shift;
70         my ($sec,$min,$hour,$mday,$mon,$year) = gmtime((defined $t) ? $t : time);
71         $year += 1900;
72         my $buf = sprintf "%02d%s%04d\@%02d:%02d:%02d", $mday, $month[$mon], $year, $hour, $min, $sec;
73         return $buf;
74 }
75
76 # get a zulu time in cluster format (2300Z)
77 sub ztime
78 {
79         my $t = shift;
80         $t = defined $t ? $t : time;
81         my $dst = shift;
82         my ($sec,$min,$hour) = $dst ? localtime($t): gmtime($t);
83         my $buf = sprintf "%02d%02d%s", $hour, $min, ($dst) ? '' : 'Z';
84         return $buf;
85 }
86
87 # get a cluster format date (23-Jun-1998)
88 sub cldate
89 {
90         my $t = shift;
91         $t = defined $t ? $t : time;
92         my $dst = shift;
93         my ($sec,$min,$hour,$mday,$mon,$year) = $dst ? localtime($t) : gmtime($t);
94         $year += 1900;
95         my $buf = sprintf "%2d-%s-%04d", $mday, $month[$mon], $year;
96         return $buf;
97 }
98
99 # return a cluster style date time
100 sub cldatetime
101 {
102         my $t = shift;
103         my $dst = shift;
104         my $date = cldate($t, $dst);
105         my $time = ztime($t, $dst);
106         return "$date $time";
107 }
108
109 # return a unix date from a cluster date and time
110 sub cltounix
111 {
112         my $date = shift;
113         my $time = shift;
114         my ($thisyear) = (gmtime)[5] + 1900;
115
116         return 0 unless $date =~ /^\s*(\d+)-(\w\w\w)-([12][90]\d\d)$/;
117         return 0 if $3 > 2036;
118         return 0 unless abs($thisyear-$3) <= 1;
119         $date = "$1 $2 $3";
120         return 0 unless $time =~ /^([012]\d)([012345]\d)Z$/;
121         $time = "$1:$2 +0000";
122         my $r = str2time("$date $time");
123         return $r unless $r;
124         return $r == -1 ? undef : $r;
125 }
126
127 # turn a latitude in degrees into a string
128 sub slat
129 {
130         my $n = shift;
131         my ($deg, $min, $let);
132         $let = $n >= 0 ? 'N' : 'S';
133         $n = abs $n;
134         $deg = int $n;
135         $min = int ((($n - $deg) * 60) + 0.5);
136         return "$deg $min $let";
137 }
138
139 # turn a longitude in degrees into a string
140 sub slong
141 {
142         my $n = shift;
143         my ($deg, $min, $let);
144         $let = $n >= 0 ? 'E' : 'W';
145         $n = abs $n;
146         $deg = int $n;
147         $min = int ((($n - $deg) * 60) + 0.5);
148         return "$deg $min $let";
149 }
150
151 # turn a true into 'yes' and false into 'no'
152 sub yesno
153 {
154         my $n = shift;
155         return $n ? $main::yes : $main::no;
156 }
157
158 # provide a data dumpered version of the object passed
159 sub dd
160 {
161         my $value = shift;
162         my $dd = new Data::Dumper([$value]);
163         $dd->Indent(0);
164         $dd->Terse(1);
165     $dd->Quotekeys($] < 5.005 ? 1 : 0);
166         $value = $dd->Dumpxs;
167         $value =~ s/([\r\n\t])/sprintf("%%%02X", ord($1))/eg;
168         $value =~ s/^\s*\[//;
169     $value =~ s/\]\s*$//;
170         
171         return $value;
172 }
173
174 # format a prompt with its current value and return it with its privilege
175 sub promptf
176 {
177         my ($line, $value, $promptl) = @_;
178         my ($priv, $prompt, $action) = split ',', $line;
179
180         # if there is an action treat it as a subroutine and replace $value
181         if ($action) {
182                 my $q = qq{\$value = $action(\$value)};
183                 eval $q;
184         } elsif (ref $value) {
185                 $value = dd($value);
186         }
187         $promptl ||= 15;
188         $prompt = sprintf "%${promptl}s: %s", $prompt, $value;
189         return ($priv, $prompt);
190 }
191
192 # turn a hex field into printed hex
193 sub phex
194 {
195         my $val = shift;
196         return sprintf '%X', $val;
197 }
198
199 # take an arg as a hash of call=>time pairs and print it
200 sub ptimelist
201 {
202         my $ref = shift;
203         my $out;
204         for (sort keys %$ref) {
205                 $out .= "$_=" . atime($ref->{$_}) . ", ";
206         }
207         chop $out;
208         chop $out;
209         return $out;    
210 }
211
212 # take an arg as an array list and print it
213 sub parray
214 {
215         my $ref = shift;
216         return ref $ref ? join(', ', sort @{$ref}) : $ref;
217 }
218
219 # take the arg as an array reference and print as a list of pairs
220 sub parraypairs
221 {
222         my $ref = shift;
223         my $i;
224         my $out;
225
226         for ($i = 0; $i < @$ref; $i += 2) {
227                 my $r1 = @$ref[$i];
228                 my $r2 = @$ref[$i+1];
229                 $out .= "$r1-$r2, ";
230         }
231         chop $out;                                      # remove last space
232         chop $out;                                      # remove last comma
233         return $out;
234 }
235
236 # take the arg as a hash reference and print it out as such
237 sub phash
238 {
239         my $ref = shift;
240         my $out;
241
242         foreach my $k (sort keys %$ref) {
243                 $out .= "${k}=>$ref->{$k}, ";
244         }
245         $out =~ s/, $// if $out;
246         return $out;
247 }
248
249 sub _sort_fields
250 {
251         my $ref = shift;
252         my @a = split /,/, $ref->field_prompt(shift); 
253         my @b = split /,/, $ref->field_prompt(shift); 
254         return lc $a[1] cmp lc $b[1];
255 }
256
257 # print all the fields for a record according to privilege
258 #
259 # The prompt record is of the format '<priv>,<prompt>[,<action>'
260 # and is expanded by promptf above
261 #
262 sub print_all_fields
263 {
264         my $self = shift;                       # is a dxchan
265         my $ref = shift;                        # is a thingy with field_prompt and fields methods defined
266         my @out;
267         my @fields = $ref->fields;
268         my $field;
269         my $width = $self->width - 1;
270         my $promptl = 0;
271         $width ||= 80;
272
273         # find the maximum length of the prompt
274         foreach $field (@fields) {
275                 if (defined $ref->{$field}) {
276                         my (undef, $prompt, undef) = split ',', $ref->field_prompt($field);
277                         $promptl = length $prompt if length $prompt > $promptl;
278                 }
279         }
280
281         # now do print
282         foreach $field (sort {_sort_fields($ref, $a, $b)} @fields) {
283                 if (defined $ref->{$field}) {
284                         my ($priv, $ans) = promptf($ref->field_prompt($field), $ref->{$field}, $promptl);
285                         my @tmp;
286                         if (length $ans > $width) {
287                                 $Text::Wrap::columns = $width-2;
288                                 my ($p, $a) = split /: /, $ans, 2;
289                                 @tmp = split/\n/, Text::Wrap::wrap("$p: ", (' ' x $promptl) . ': ', $a);
290                         } else {
291                                 push @tmp, $ans;
292                         }
293                         push @out, @tmp if ($self->priv >= $priv);
294                 }
295         }
296         return @out;
297 }
298
299 # generate a regex from a shell type expression 
300 # see 'perl cookbook' 6.9
301 sub shellregex
302 {
303         my $in = shift;
304         $in =~ s{(.)} { $patmap{$1} || "\Q$1" }ge;
305         $in =~ s|\\/|/|g;
306         return '^' . $in . "\$";
307 }
308
309 # read in a file into a string and return it. 
310 # the filename can be split into a dir and file and the 
311 # file can be in upper or lower case.
312 # there can also be a suffix
313 sub readfilestr
314 {
315         my ($dir, $file, $suffix) = @_;
316         my $fn;
317         my $f;
318         if ($suffix) {
319                 $f = uc $file;
320                 $fn = "$dir/$f.$suffix";
321                 unless (-e $fn) {
322                         $f = lc $file;
323                         $fn = "$dir/$file.$suffix";
324                 }
325         } elsif ($file) {
326                 $f = uc $file;
327                 $fn = "$dir/$file";
328                 unless (-e $fn) {
329                         $f = lc $file;
330                         $fn = "$dir/$file";
331                 }
332         } else {
333                 $fn = $dir;
334         }
335
336         my $fh = new IO::File $fn;
337         my $s = undef;
338         if ($fh) {
339                 local $/ = undef;
340                 $s = <$fh>;
341                 $fh->close;
342         }
343         return $s;
344 }
345
346 # write out a file in the format required for reading
347 # in via readfilestr, it expects the same arguments 
348 # and a reference to an object
349 sub writefilestr
350 {
351         my $dir = shift;
352         my $file = shift;
353         my $suffix = shift;
354         my $obj = shift;
355         my $fn;
356         my $f;
357         
358         confess('no object to write in writefilestr') unless $obj;
359         confess('object not a reference in writefilestr') unless ref $obj;
360         
361         if ($suffix) {
362                 $f = uc $file;
363                 $fn = "$dir/$f.$suffix";
364                 unless (-e $fn) {
365                         $f = lc $file;
366                         $fn = "$dir/$file.$suffix";
367                 }
368         } elsif ($file) {
369                 $f = uc $file;
370                 $fn = "$dir/$file";
371                 unless (-e $fn) {
372                         $f = lc $file;
373                         $fn = "$dir/$file";
374                 }
375         } else {
376                 $fn = $dir;
377         }
378
379         my $fh = new IO::File ">$fn";
380         if ($fh) {
381                 my $dd = new Data::Dumper([ $obj ]);
382                 $dd->Indent(1);
383                 $dd->Terse(1);
384                 $dd->Quotekeys(0);
385                 #       $fh->print(@_) if @_ > 0;     # any header comments, lines etc
386                 $fh->print($dd->Dumpxs);
387                 $fh->close;
388         }
389 }
390
391 sub filecopy
392 {
393         copy(@_) or return $!;
394 }
395
396 # remove leading and trailing spaces from an input string
397 sub unpad
398 {
399         my $s = shift;
400         $s =~ s/^\s*//;
401         $s =~ s/\s*$//;
402         return $s;
403 }
404
405 # check that a field only has callsign characters in it
406 sub is_callsign
407 {
408         return $_[0] =~ m!^
409                                           (?:\d?[A-Z]{1,2}\d{0,2}/)?    # out of area prefix /  
410                                           (?:\d?[A-Z]{1,2}\d{1,5})      # main prefix one (required) - lengthened for special calls 
411                                           [A-Z]{1,8}                # callsign letters (required)
412                                           (?:-(?:\d{1,2}))?         # - nn possibly (eg G8BPQ-8)
413                                           (?:/[0-9A-Z]{1,7})?       # / another prefix, callsign or special label (including /MM, /P as well as /EURO or /LGT) possibly
414                                           (?:/(?:AM?|MM?|P))?       # finally /A /AM /M /MM /P 
415                                           $!xo;
416
417         # longest callign allowed is 1X11/1Y11XXXXX-11/XXXXXXX/MM
418 }
419
420 sub is_prefix
421 {
422         return $_[0] =~ m!^(?:[A-Z]{1,2}\d+ | \d[A-Z]{1,2}}\d+)!x        # basic prefix
423 }
424         
425
426 # check that a PC protocol field is valid text
427 sub is_pctext
428 {
429         return undef unless length $_[0];
430         return undef if $_[0] =~ /[\x00-\x08\x0a-\x1f\x80-\x9f]/;
431         return 1;
432 }
433
434 # check that a PC prot flag is fairly valid (doesn't check the difference between 1/0 and */-)
435 sub is_pcflag
436 {
437         return $_[0] =~ /^[01\*\-]+$/;
438 }
439
440 # check that a thing is a frequency
441 sub is_freq
442 {
443         return $_[0] =~ /^\d+(?:\.\d+)?$/;
444 }
445
446 # check that a thing is just digits
447 sub is_digits
448 {
449         return $_[0] =~ /^[\d]+$/;
450 }
451
452 # does it look like a qra locator?
453 sub is_qra
454 {
455         return unless length $_[0] == 4 || length $_[0] == 6;
456         return $_[0] =~ /^[A-Ra-r][A-Ra-r]\d\d(?:[A-Xa-x][A-Xa-x])?$/;
457 }
458
459 # does it look like a valid lat/long
460 sub is_latlong
461 {
462         return $_[0] =~ /^\s*\d{1,2}\s+\d{1,2}\s*[NnSs]\s+1?\d{1,2}\s+\d{1,2}\s*[EeWw]\s*$/;
463 }
464
465 # is it an ip address?
466 sub is_ipaddr
467 {
468         $_[0] =~ s|/\d+$||;
469         # if ($ptonok) {
470         #       if ($_[0] =~ /:/) {
471         #               if (inet_pton(AF_INET6, $_[0])) {
472         #                       return ($_[0] =~ /([:0-9a-f]+)/);
473         #               }
474         #       } else {
475         #               if (inet_pton(AF_INET, $_[0])) {
476         #                       return ($_[0] =~ /([\.\d]+)/);
477         #               }
478         #       }
479         # } else {
480                 if ($_[0] =~ /:/) {
481                         return ($_[0] =~ /^((?:\:?\:?[0-9a-f]{0,4}){1,8}\:?\:?)$/i);    
482                 } else {
483                         return ($_[0] =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
484                 }
485 #       }
486         return undef;
487 }
488
489 # is it a zulu time hhmmZ
490 sub is_ztime
491 {
492         return $_[0] =~ /^(?:(?:2[0-3])|(?:[01][0-9]))[0-5][0-9]Z$/;
493 }
494
495 # insert an item into a list if it isn't already there returns 1 if there 0 if not
496 sub insertitem
497 {
498         my $list = shift;
499         my $item = shift;
500         
501         return 1 if grep {$_ eq $item } @$list;
502         push @$list, $item;
503         return 0;
504 }
505
506 # delete an item from a list if it is there returns no deleted 
507 sub deleteitem
508 {
509         my $list = shift;
510         my $item = shift;
511         my $n = @$list;
512         
513         @$list = grep {$_ ne $item } @$list;
514         return $n - @$list;
515 }
516
517 # find the correct local_data directory
518 # basically, if there is a local_data directory with this filename and it is younger than the
519 # equivalent one in the (system) data directory then return that name rather than the system one
520 sub localdata
521 {
522         my $ifn = shift;
523         my $lfn = "$main::local_data/$ifn";
524         my $dfn =  "$main::data/$ifn";
525         
526         if (-e "$main::local_data") {
527                 if ((-e $dfn) && (-e $lfn)) {
528                         $lfn = $dfn if -M $dfn < -M $lfn;
529                 } else {
530                         $lfn = $dfn if -e $dfn;
531                 }
532         } else {
533                 $lfn = $dfn;
534         }
535
536         return $lfn;
537 }
538
539 # move a file or a directory from data -> local_data if isn't there already
540 sub localdata_mv
541 {
542         my $ifn = shift;
543         if (-e "$main::data/$ifn" ) {
544                 unless (-e "$main::local_data/$ifn") {
545                         move("$main::data/$ifn", "$main::local_data/$ifn") or die "localdata_mv: cannot move $ifn from '$main::data' -> '$main::local_data' $!\n";
546                 }
547         }
548 }
549
550 # measure the time taken for something to happen; use Time::HiRes qw(gettimeofday tv_interval);
551 sub _diffms
552 {
553         my $ta = shift;
554         my $tb = shift || [gettimeofday];
555         my $a = int($ta->[0] * 1000) + int($ta->[1] / 1000); 
556         my $b = int($tb->[0] * 1000) + int($tb->[1] / 1000);
557         return $b - $a;
558 }
559
560 # and in microseconds
561 sub _diffus
562 {
563         my $ta = shift;
564         my $tb = shift || [gettimeofday];
565         my $a = int($ta->[0] * 1000000) + int($ta->[1]); 
566         my $b = int($tb->[0] * 1000000) + int($tb->[1]);
567         return $b - $a;
568 }
569
570 sub diffms
571 {
572         my $call = shift;
573         my $line = shift;
574         my $ta = shift;
575         my $no = shift;
576         my $tb = shift;
577         my $msecs = _diffms($ta, $tb);
578
579         $line =~ s|\s+$||;
580         my $s = "subprocess stats cmd: '$line' $call ${msecs}mS";
581         $s .= " $no lines" if $no;
582         DXDebug::dbg($s);
583 }
584
585 # expects either an array reference or two times (in the correct order [start, end])
586 sub difft
587 {
588         my $b = shift;
589         my $adds = shift // 0;
590         
591         my $t;
592         if (ref $b eq 'ARRAY') {
593                 $t = $b->[1] - $b->[0];
594         } else {
595                 if ($adds && $adds =~ /^\d+$/ && $adds >= $b) {
596                         $t = $adds - $b;
597                         $adds = shift;
598                 } else {
599                         $t = $main::systime - $b;
600                 }
601         }
602         return '-(ve)' if $t < 0;
603         my ($y,$d,$h,$m,$s);
604         my $out = '';
605         $y = int $t / (86400*365);
606         $out .= sprintf ("%s${y}y", $adds?' ':'') if $y;
607         $t -= $y * 86400 * 365;
608         $d = int $t / 86400;
609         $out .= sprintf ("%s${d}d", $adds?' ':'') if $d;
610         $t -= $d * 86400;
611         $h = int $t / 3600;
612         $out .= sprintf ("%s${h}h", $adds?' ':'') if $h;
613         $t -= $h * 3600;
614         $m = int $t / 60;
615         $out .= sprintf ("%s${m}m", $adds?' ':'') if $m;
616         if (($d == 0 && $adds) || (int $adds && $adds == 2)) {
617                 $s = int $t % 60;
618                 $out .= sprintf ("%s${s}s", $adds?' ':'') if $s;
619                 $out ||= sprintf ("%s0s", $adds?' ':'');
620         }
621         $out = '0s' unless length $out;
622         return $out;
623 }
624
625 # print an array ref of difft refs
626 sub parraydifft
627 {
628         my $r = shift;
629         my $out = '';
630         for (@$r) {
631                 my $s = $_->[2] ? "($_->[2])" : '';
632                 $out .= sprintf "%s=%s$s, ", atime($_->[0]), difft($_->[0], $_->[1]);
633         }
634         $out =~ s/,\s*$//;
635         return $out;
636 }
637
638 sub basecall
639 {
640         my ($r) = $_[0] =~ m{^((?:[\w\d]+/)?[\w\d]+(?:/[\w\d]+)*)(?:-\d+)?(?:-\#)?$};
641         return $r;
642 }
643
644 sub normalise_call
645 {
646         my ($c, $ssid) = $_[0] =~ m|^((?:[\w\d]+/)?[\d\w]+(?:/[\w\d]+)*)(?:-(\d+))?(?:-\#)?$|;
647         my $ncall = $c;
648         $ssid += 0;
649         $ncall .= "-$ssid" if $ssid;
650         return $ncall;
651 }
652
653 sub is_numeric
654 {
655         return $_[0] =~ /^[\.\d]+$/;
656 }
657
658 1;