File Coverage

blib/lib/Zeek/Log/Parse.pm
Criterion Covered Total %
statement 96 114 84.2
branch 37 50 74.0
condition 12 24 50.0
subroutine 13 13 100.0
pod 2 4 50.0
total 160 205 78.0


line stmt bran cond sub pod time code
1             package Zeek::Log::Parse;
2             # ABSTRACT: Perl interface for parsing Zeek logfiles
3              
4 8     8   442588 use strict;
  8         55  
  8         210  
5 8     8   46 use warnings;
  8         12  
  8         241  
6 8     8   84 use 5.10.1;
  8         19  
7              
8             # use Exporter;
9 8     8   3203 use autodie;
  8         102562  
  8         29  
10 8     8   43673 use Carp;
  8         13  
  8         371  
11 8     8   39 use Scalar::Util qw/openhandle/;
  8         13  
  8         940  
12              
13             our $VERSION = '0.08';
14              
15             #@EXPORT_OK = qw//;
16              
17             my $json = eval {
18             require JSON;
19             JSON->import();
20             1;
21             }; # true if we support reading from json
22              
23             BEGIN {
24 8     8   38 my @accessors = qw/fh file line headers headerlines fields/;
25              
26 8         18 for my $accessor ( @accessors ) {
27 8     8   49 no strict 'refs';
  8         13  
  8         571  
28             *$accessor = sub {
29 8     8   617 my $self = shift;
30 8         31 return $self->{$accessor};
31             }
32 48         8480 }
33              
34             }
35              
36             sub new {
37 11     11 1 3429 my $class = shift;
38 11         16 my $arg = shift;
39              
40 11         19 my $self = {};
41 11         26 $self->{line} = undef;
42              
43 11 100       60 if ( !defined($arg) ) {
    100          
    100          
44 1         2 $self->{diamond} = 1;
45             } elsif ( ref($arg) eq 'HASH' ) {
46 5         9 $self = $arg;
47             } elsif ( defined(openhandle($arg)) ) {
48 1         3 $self->{fh} = $arg;
49             } else {
50 4         9 $self->{file} = $arg;
51             }
52              
53 11         20 bless $self, $class;
54              
55 11 100 66     70 if ( defined($self->{file}) && !(defined($self->{fh})) ) {
56 6 50       117 unless ( -f $self->{file} ) {
57 0         0 croak("Could not open ".$self->{file});
58             }
59              
60             open( my $fh, "<", $self->{file} )
61 6 50       91 or croak("Cannot open ".$self->{file});
62 6         12877 $self->{fh} = $fh;
63             }
64              
65 11 100 66     43 if ( !defined($self->{fh}) && ( !defined($self->{diamond}) || !$self->{diamond} ) ) {
      100        
66 1         165 croak("No filename given in constructor. Aborting");
67             }
68              
69 10         40 $self->{json_file} = 0;
70 10         31 $self->{names} = [ $self->readheader() ];
71 10         27 $self->{fields} = $self->{names};
72 10   100     49 $self->{empty_as_undef} //= 0;
73              
74 10   50     38 $self->{headers} //= {};
75 10   50     35 $self->{headerlines} //= [];
76              
77 10         54 return $self;
78             }
79              
80             sub readheader {
81 10     10 0 16 my $self = shift;
82              
83 10         17 my @headerlines;
84             my @names;
85 10         15 my $firstline = 1;
86             # first: read header line. This is a little brittle, but... welll... well, it is.
87 10         23 while ( my $line = $self->extractNextLine() ) {
88 80 100       126 if ( $firstline ) {
89 10         17 $firstline = 0;
90 10 50 33     62 if ( length($line) > 1 && substr($line, 0, 1) eq "{" ) {
91             # Json file. stuff line in saved_line and try to extract header fields...
92 0 0       0 croak("Parsing json formatted log files needs JSON module") unless ( $json );
93 0         0 my $val = decode_json($line);
94 0         0 $self->{saved_line} = $line;
95 0 0 0     0 if ( !defined($val) || ref($val) ne "HASH" ) {
96 0         0 croak("Error parsing first line of json formatted log - $line");
97             }
98 0         0 $self->{json_file} = 1;
99 0         0 return sort keys %$val;
100             }
101             }
102 80         94 chomp($line);
103 80         124 push(@headerlines, $line);
104              
105 80         201 my @fields = split /\t/,$line;
106              
107 80 50       189 unless ( $line =~ /^#/ ) {
108 0         0 croak("Did not find required fields and types header lines: $line");
109             }
110              
111 80         99 my $type = shift(@fields);
112 80 100       204 if ( "#fields" eq $type ) {
    100          
113             # yay.
114             # we have our field names...
115 10         47 @names = @fields;
116             } elsif ( "#types" eq $type) {
117 10         26 last;
118             }
119             }
120              
121 10         23 $self->{headerlines} = \@headerlines;
122 10         28 $self->{headers} = { map {/#(\w+)\s+(.*)/;$1=>$2} @headerlines };
  80         169  
  80         310  
123              
124 10         72 return @names;
125             }
126              
127              
128             sub getLine {
129 22     22 1 33218 my $self = shift;
130              
131 22         38 my @names = @{$self->{names}};
  22         91  
132              
133 22         51 while ( my $line = $self->extractNextLine ) {
134 31         50 my $removed = chomp($line);
135 31         53 $self->{line} = $line;
136              
137 31 50       58 if ( $self->{json_file} ) {
138 0         0 my $val = decode_json($line);
139 0 0 0     0 if ( !defined($val) || ref($val) ne "HASH" ) {
140 0         0 croak("Error parsing line of json formatted log - $line");
141             }
142 0         0 $self->{names} = [ sort keys %$val ];
143 0         0 return $val;
144             }
145              
146 31         134 my @fields = split "\t", $line;
147              
148 31 100       97 if ( $line =~ /^#/ ) {
149 12 100       26 if ( "#fields" eq shift(@fields) ) {
150 1         4 @names = @fields;
151 1         2 $self->{names} = \@fields;
152             # This is not really nice, but for the moment we do not really need any
153             # of the other header lines for parsing files - and we do not keep track
154             # of them. Sorry...
155 1         6 $self->{headers} = [ join("\t", ("#fields", @fields)) ];
156             }
157 12         28 next;
158             }
159 19         27 my %f;
160              
161 19 100       54 unless (scalar @names == scalar @fields) {
162 1 50       5 next if ( $removed == 0 );
163 0         0 croak("Number of expected fields does not match number of fields in file");
164             }
165              
166 18         36 for my $name ( @names ) {
167 360         402 my $field = shift(@fields);
168 360 100       522 if ( ( $field eq "-" ) ) {
    100          
169 90         150 $f{$name} = undef;
170             } elsif ( $field eq "(empty)" ) {
171 14 100       55 $f{$name} = $self->{empty_as_undef} ? undef : [];
172             } else {
173 256         433 $f{$name} = $field;
174             }
175             }
176              
177 18         99 return \%f;
178             }
179             }
180              
181             sub extractNextLine {
182 115     115 0 155 my $self = shift;
183              
184 115 50       189 if( defined($self->{saved_line}) ) {
185 0         0 my $sl = $self->{saved_line};
186 0         0 undef $self->{saved_line};
187 0         0 return $sl;
188             }
189              
190 115         141 my $in = $self->{fh};
191              
192 115 100       714 return defined($in) ? <$in> : <>;
193             }
194              
195             1;