File Coverage

blib/lib/NcFTPd/Log/Parse/Base.pm
Criterion Covered Total %
statement 52 56 92.8
branch 15 22 68.1
condition 9 17 52.9
subroutine 13 15 86.6
pod 0 3 0.0
total 89 113 78.7


line stmt bran cond sub pod time code
1             package NcFTPd::Log::Parse::Base;
2            
3 4     4   23 use strict;
  4         8  
  4         122  
4 4     4   18 use warnings;
  4         7  
  4         94  
5            
6 4     4   3428 use IO::File;
  4         53324  
  4         748  
7 4     4   35 use Carp;
  4         8  
  4         3669  
8            
9             my @TRANSFER_STATUSES = qw{OK ABOR INCOMPLETE PERM NOENT ERROR};
10             my %COMMON_REGEX = (
11             time => '\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?', # Date and time, optional millisecond precision
12             process => '\#u\d+|\([a-z]+\)',
13             decimal => '\d+(?:\.\d+)?',
14             session => '[+/0-9A-Za-z]+',
15             status => join '|', @TRANSFER_STATUSES
16             );
17            
18             sub new
19             {
20 15     15 0 19620 my ($class, $file, %options) = @_;
21            
22 15 50       44 croak "usage: $class->new(\$file [, \%options ])" unless $file;
23 15 100       1112 croak "$file is a directory" if -d $file; # On some platforms IO::File will gladly open a directory
24 12 50 66     63 croak 'filter must be a CODE ref' if defined $options{filter} && ref $options{filter} ne 'CODE';
25            
26 12   33     97 my $log = IO::File->new($file, '<') || croak "Error opening file $file: $!";
27            
28             bless {
29             log => $log,
30             error => '',
31 16     16   74 filter => $options{filter} || sub { 1 },
32 12   100     1882 expand => $options{expand}
33             }, $class;
34             }
35            
36             sub next
37             {
38 25     25 0 266 my $self = shift;
39 25         26 my $entry;
40            
41 25         86 while($entry = $self->_next_entry) {
42 30         70 local $_ = $entry;
43 30 100       75 last if $self->{filter}->();
44             }
45            
46 25         84 $entry;
47             }
48            
49             sub error
50             {
51 5     5 0 39 (shift)->{error}
52             }
53            
54             sub _next_entry
55             {
56 35     35   97 my $self = shift;
57 35         70 $self->{error} = '';
58            
59 35 100       70 my $line = $self->_next_line or return;
60 30         87 my $entry = $self->_parse_line($line);
61            
62             # Don't squash an error message set by a subclass
63 30 50 33     75 $self->{error} = 'Cannot parse line: unrecognized format'
64             unless $entry or $self->{error};
65            
66 30         86 $entry;
67             }
68            
69             sub _next_line
70             {
71 35     35   42 my $self = shift;
72 35         46 my $log = $self->{log};
73 35         1060 my $line = $log->getline;
74            
75 35 50 66     1317 $self->{error} = "Error reading log file: $!"
76             unless defined $line or $log->eof;
77            
78 35         161 $line;
79             }
80            
81             sub _parse_line
82             {
83 30     30   45 my ($self, $line) = @_;
84            
85 30 50 33     578 return unless $line and
86             $line =~ m{^($COMMON_REGEX{time})\s($COMMON_REGEX{process})\s+\|\s(.+)};
87 30         71 my $time = $1;
88 30         48 my $pid = $2;
89 30         104 my $entry = $self->_parse_entry($3);
90            
91 30 50       68 if($entry) {
92 30         60 $entry->{time} = $time;
93 30         54 $entry->{process} = $pid;
94            
95 30 100       74 if($self->{expand}) {
96 0         0 my @fields = ref($self->{expand}) eq 'ARRAY'
97 7 50       56 ? @{$self->{expand}}
98             : keys %$entry;
99            
100 7         19 for my $field (@fields) {
101 91         263 $entry->{$field} = $self->_expand_field($field, $entry->{$field});
102             }
103             }
104             }
105            
106 30         73 $entry;
107             }
108            
109             sub _expand_field
110             {
111 0     0   0 my ($self, $name, $value) = @_;
112            
113             # Default behavior, subclasses might not expand anything
114 0         0 $value;
115             }
116            
117             sub _transfer_statuses
118             {
119 2     2   11 @TRANSFER_STATUSES;
120             }
121            
122             sub _common_regex
123             {
124 4     4   30 %COMMON_REGEX;
125             }
126            
127             sub _parse_entry
128             {
129 0     0     croak __PACKAGE__, '->_parse_entry is abstract';
130             }
131            
132             1;