| |
1 ## |
| |
2 ## lint-fsl.pl -- OpenPKG fsl.* File Checker |
| |
3 ## Copyright (c) 2000-2012 OpenPKG GmbH <http://openpkg.com/> |
| |
4 ## |
| |
5 ## This software is property of the OpenPKG GmbH, DE MUC HRB 160208. |
| |
6 ## All rights reserved. Licenses which grant limited permission to use, |
| |
7 ## copy, modify and distribute this software are available from the |
| |
8 ## OpenPKG GmbH. |
| |
9 ## |
| |
10 ## THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED |
| |
11 ## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| |
12 ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
| |
13 ## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR |
| |
14 ## CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| |
15 ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| |
16 ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
| |
17 ## USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| |
18 ## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| |
19 ## OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT |
| |
20 ## OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| |
21 ## SUCH DAMAGE. |
| |
22 ## |
| |
23 |
| |
24 # Perl run-time requirement |
| |
25 require 5; |
| |
26 BEGIN { |
| |
27 eval "use Getopt::Long; use IO;"; |
| |
28 if ($@) { |
| |
29 print STDERR |
| |
30 "lint-fsl: ERROR: This command requires a full-size Perl installation!\n" . |
| |
31 "lint-fsl: HINT: Install OpenPKG \"perl\" package to use this command.\n"; |
| |
32 exit(1); |
| |
33 } |
| |
34 } |
| |
35 |
| |
36 # OpenPKG instance prefix |
| |
37 my $my_prefix = $ENV{'OPENPKG_PREFIX'}; |
| |
38 delete $ENV{'OPENPKG_PREFIX'}; |
| |
39 |
| |
40 # program information |
| |
41 my $progname = "lint-fsl"; |
| |
42 my $progvers = "1.0.0"; |
| |
43 |
| |
44 # parameters (defaults) |
| |
45 my $version = 0; |
| |
46 my $verbose = 0; |
| |
47 my $help = 0; |
| |
48 my $check = 'all'; |
| |
49 my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp") . "/$progname"; |
| |
50 my $rpm = "$my_prefix/bin/openpkg rpm"; |
| |
51 |
| |
52 # exception handling support |
| |
53 $SIG{__DIE__} = sub { |
| |
54 my ($err) = @_; |
| |
55 $err =~ s|\s+at\s+.*||s if (not $verbose); |
| |
56 print STDERR "$progname:ERROR: $err ". ($! ? "($!)" : "") . "\n"; |
| |
57 exit(1); |
| |
58 }; |
| |
59 |
| |
60 # command line parsing |
| |
61 Getopt::Long::Configure("bundling"); |
| |
62 my $result = GetOptions( |
| |
63 'V|version' => \$version, |
| |
64 'v|verbose' => \$verbose, |
| |
65 'h|help' => \$help, |
| |
66 'c|check=s' => \$check, |
| |
67 't|tmpdir=s' => \$tmpdir, |
| |
68 'r|rpm=s' => \$rpm, |
| |
69 ) || die "option parsing failed"; |
| |
70 if ($help) { |
| |
71 print "Usage: $progname [options] [RPMFILE ...]\n" . |
| |
72 "Available options:\n" . |
| |
73 " -v,--verbose enable verbose run-time mode\n" . |
| |
74 " -h,--help print out this usage page\n" . |
| |
75 " -c,--check=CHECKS select checks to perform (default='all')\n" . |
| |
76 " -r,--rpm=FILE filesystem path to RPM program\n" . |
| |
77 " -t,--tmpdir=PATH filesystem path to temporary directory\n" . |
| |
78 " -V,--version print program version\n"; |
| |
79 exit(0); |
| |
80 } |
| |
81 if ($version) { |
| |
82 print "OpenPKG $progname $progvers\n"; |
| |
83 exit(0); |
| |
84 } |
| |
85 |
| |
86 # verbose message printing |
| |
87 sub msg_verbose { |
| |
88 my ($msg) = @_; |
| |
89 print STDERR "$msg\n" if ($verbose); |
| |
90 } |
| |
91 |
| |
92 # warning message printing |
| |
93 sub msg_warning { |
| |
94 my ($msg) = @_; |
| |
95 print STDERR "$progname:WARNING: $msg\n"; |
| |
96 } |
| |
97 |
| |
98 # error message printing |
| |
99 sub msg_error { |
| |
100 my ($msg) = @_; |
| |
101 print STDERR "$progname:ERROR: $msg\n"; |
| |
102 } |
| |
103 |
| |
104 # determine check list |
| |
105 my @check_list = (qw( |
| |
106 blank |
| |
107 comment |
| |
108 ident |
| |
109 )); |
| |
110 my @checks = (); |
| |
111 if ($check eq 'all') { |
| |
112 @checks = @check_list; |
| |
113 } |
| |
114 else { |
| |
115 foreach my $c (split(/,/, $check)) { |
| |
116 if (not grep(/^$c$/, @check_list)) { |
| |
117 die "invalid check \"$c\""; |
| |
118 } |
| |
119 push(@checks, $c); |
| |
120 } |
| |
121 } |
| |
122 |
| |
123 # global return code |
| |
124 $main::GRC = 0; |
| |
125 |
| |
126 # environment preparation |
| |
127 system("rm -rf $tmpdir"); |
| |
128 system("mkdir -p $tmpdir"); |
| |
129 |
| |
130 # iterate over all fsl. files |
| |
131 foreach my $filename (@ARGV) { |
| |
132 my $io = new IO::File "<$filename" |
| |
133 or die "unable to open file \"$filename\" for reading"; |
| |
134 my $spec; { local $/ = undef; $spec = <$io>; } |
| |
135 $io->close; |
| |
136 foreach my $check (@checks) { |
| |
137 eval "\&check_$check(\$filename, \$spec);"; |
| |
138 } |
| |
139 } |
| |
140 |
| |
141 # environment cleanup |
| |
142 system("rm -rf $tmpdir"); |
| |
143 |
| |
144 # die gracefully |
| |
145 exit($main::GRC); |
| |
146 |
| |
147 ## _________________________________________________________________ |
| |
148 ## |
| |
149 ## COMMON SUBROUTINES |
| |
150 ## _________________________________________________________________ |
| |
151 ## |
| |
152 |
| |
153 sub lines { |
| |
154 my ($txt) = @_; |
| |
155 my $l = 0; |
| |
156 $txt =~ s|\n|$l++, ''|sge; |
| |
157 return $l; |
| |
158 } |
| |
159 |
| |
160 sub lint_message { |
| |
161 my ($type, $file, $done, $this, $msg) = @_; |
| |
162 if (defined($done) and defined($this)) { |
| |
163 my $start = &lines($done) + 1; |
| |
164 my $end = $start + &lines($this); |
| |
165 my $pos = $start; |
| |
166 $pos .= "-". $end if ($end > $start); |
| |
167 printf("%s:%s: %s:%s: %s\n", $progname, $type, $file, $pos, $msg); |
| |
168 } |
| |
169 else { |
| |
170 printf("%s:%s: %s: %s\n", $progname, $type, $file, $msg); |
| |
171 } |
| |
172 } |
| |
173 |
| |
174 sub lint_warning { |
| |
175 my ($file, $done, $this, $msg) = @_; |
| |
176 &lint_message("WARNING", $file, $done, $this, $msg); |
| |
177 $main::GRC = 1 if ($main::GRC < 1); |
| |
178 } |
| |
179 |
| |
180 sub lint_error { |
| |
181 my ($file, $done, $this, $msg) = @_; |
| |
182 &lint_message("ERROR", $file, $done, $this, $msg); |
| |
183 $main::GRC = 2 if ($main::GRC < 2); |
| |
184 } |
| |
185 |
| |
186 ## _________________________________________________________________ |
| |
187 ## |
| |
188 ## CHECK "blank": whitespace and blank lines |
| |
189 ## _________________________________________________________________ |
| |
190 ## |
| |
191 |
| |
192 sub check_blank { |
| |
193 my ($file, $spec) = @_; |
| |
194 |
| |
195 # check for CR-LF combination |
| |
196 my $done = ''; my $this = ''; my $todo = $spec; |
| |
197 while ($todo =~ m/\r\n/s) { |
| |
198 $done .= $`; $this = $&; $todo = $'; |
| |
199 &lint_warning($file, $done, $this, "carriage-return (CR, 0x0d) line-feed (NL, 0x0a) combination (expected just line-feed)"); |
| |
200 $done .= $this; |
| |
201 } |
| |
202 |
| |
203 # check for multiple blank lines |
| |
204 $done = ''; $this = ''; $todo = $spec; |
| |
205 while ($todo =~ m/(\r?\n[ \t]*){3,}/s) { |
| |
206 $done .= $`; $this = $&; $todo = $'; |
| |
207 &lint_warning($file, $done, $this, "multiple subsequent blank lines (expected single blank line)"); |
| |
208 $done .= $this; |
| |
209 } |
| |
210 |
| |
211 # check for trailing whitespaces |
| |
212 $done = ''; $this = ''; $todo = $spec; |
| |
213 while ($todo =~ m/[ \t]+\r?\n/s) { |
| |
214 $done .= $`; $this = $&; $todo = $'; |
| |
215 if ($done eq '' or $done =~ m|\n$|s) { |
| |
216 &lint_warning($file, $done, $this, "whitespace on empty line (expected none)"); |
| |
217 } |
| |
218 else { |
| |
219 &lint_warning($file, $done, $this, "trailing whitespace (expected none)"); |
| |
220 } |
| |
221 $done .= $this; |
| |
222 } |
| |
223 |
| |
224 # check for bogus line continuations |
| |
225 $done = ''; $this = ''; $todo = $spec; |
| |
226 while ($todo =~ m/\\[ \t]*\r?\n(?=[ \t]*\r?\n)/s) { |
| |
227 $done .= $`; $this = $&; $todo = $'; |
| |
228 &lint_warning($file, $done, $this, "bogus line continuation for following empty line (expect no line continuation)"); |
| |
229 $done .= $this; |
| |
230 } |
| |
231 |
| |
232 # check for leading whitespaces before line continuations |
| |
233 $done = ''; $this = ''; $todo = $spec; |
| |
234 while ($todo =~ m/[ \t]{2,}\\[ \t]*\r?\n/s) { |
| |
235 $done .= $`; $this = $&; $todo = $'; |
| |
236 &lint_warning($file, $done, $this, "multiple leading whitespace before line continuation (expected just a single space)"); |
| |
237 $done .= $this; |
| |
238 } |
| |
239 |
| |
240 # check for leading tabs |
| |
241 $done = ''; $this = ''; $todo = $spec; |
| |
242 while ($todo =~ m/^ *\t+ *[^ \t]/m) { |
| |
243 $done .= $`; $this = $&; $todo = $'; |
| |
244 &lint_warning($file, $done, $this, "leading tabs (expected spaces)"); |
| |
245 $done .= $this; |
| |
246 } |
| |
247 |
| |
248 # check for mandatory/wished trailing blank line |
| |
249 if ($spec !~ m|\n\n$|) { |
| |
250 &lint_warning($file, $done, "", "mandatory/wished trailing blank line missing (expected one)"); |
| |
251 } |
| |
252 } |
| |
253 |
| |
254 ## _________________________________________________________________ |
| |
255 ## |
| |
256 ## CHECK "comment": sharp-comments |
| |
257 ## _________________________________________________________________ |
| |
258 ## |
| |
259 |
| |
260 sub check_comment { |
| |
261 my ($file, $spec) = @_; |
| |
262 my ($pkg); |
| |
263 |
| |
264 # determine package name |
| |
265 $pkg = $file; |
| |
266 $pkg =~ s|^.+/||; |
| |
267 $pkg =~ s|^fsl\.||; |
| |
268 |
| |
269 # check comment header |
| |
270 my $re = ""; |
| |
271 $re .= "##\\n## fsl.$pkg -- OSSP fsl configuration\\n##\\n\\n"; |
| |
272 if ($spec !~ m|^$re|os) { |
| |
273 &lint_warning($file, "", "", "invalid comment header (expected $re)"); |
| |
274 } |
| |
275 |
| |
276 # check for comment indentation |
| |
277 my $done .= $`; my $this = $&; my $todo = $'; |
| |
278 while ($todo =~ m/^([ \t]*)(#+)([ \t]*)(.*?)$/m) { |
| |
279 $done .= $`; $this = $&; $todo = $'; |
| |
280 my ($lead, $sharp, $pad, $text) = ($1, $2, $3, $4); |
| |
281 if (length($lead) % 2 != 0) { |
| |
282 &lint_warning($file, $done, $this, "incorrect comment indentation (expected a multiple of 2 spaces)"); |
| |
283 } |
| |
284 if (length($lead) > 1 && length($sharp) > 1) { |
| |
285 &lint_warning($file, $done, $this, "indented comment has introduced with multiple sharps (expected single sharp character)"); |
| |
286 } |
| |
287 if (length($pad.$text) > 0 && length($sharp.$pad) % 4 != 0) { |
| |
288 &lint_warning($file, $done, $this, "incorrect comment text padding (expected a multiple of 4 sharps or spaces)"); |
| |
289 } |
| |
290 if (length($pad) == 0 && length($text) > 0) { |
| |
291 &lint_warning($file, $done, $this, "missing leading space before comment text (expected padding spaces)"); |
| |
292 } |
| |
293 if (length($pad) > 0 && length($text) == 0) { |
| |
294 &lint_warning($file, $done, $this, "empty comment text (expected a reasonable text)"); |
| |
295 } |
| |
296 $done .= $this; |
| |
297 } |
| |
298 } |
| |
299 |
| |
300 ## _________________________________________________________________ |
| |
301 ## |
| |
302 ## CHECK "ident" |
| |
303 ## _________________________________________________________________ |
| |
304 ## |
| |
305 |
| |
306 sub check_ident { |
| |
307 my ($file, $spec) = @_; |
| |
308 my ($pkg, $section); |
| |
309 |
| |
310 # determine package name |
| |
311 $pkg = $file; |
| |
312 $pkg =~ s|^.+/||; |
| |
313 $pkg =~ s|^fsl\.||; |
| |
314 |
| |
315 # check sections with ident/facility regex |
| |
316 my $done .= ""; my $this = ""; my $todo = $spec; |
| |
317 while ($todo =~ m:\n(\w+)(\s+)(\S+)/(\S+)(\s+)q\{(.*?)\};:s) { |
| |
318 $done .= $`; $this = $&; $todo = $'; |
| |
319 my ($section, $ws1, $ident, $facility, $ws2, $body) = ($1, $2, $3, $4, $5, $6); |
| |
320 |
| |
321 if ($pkg eq "fsl") { |
| |
322 # enforce default section for fsl |
| |
323 if ($section ne "default") { |
| |
324 &lint_warning($file, "", "", "section \"$section\" not allowed for package $pkg (expected default)"); |
| |
325 } |
| |
326 } |
| |
327 else { |
| |
328 # enforce ident section for any package othen than fsl |
| |
329 if ($section ne "ident") { |
| |
330 &lint_warning($file, "", "", "section \"$section\" not allowed for package $pkg (expected ident)"); |
| |
331 } |
| |
332 |
| |
333 # ident and facility wildcard-only would be a catch-all |
| |
334 if ($ident =~ m/^[(]?\.[\+\*][)]?$/ and $facility =~ m/^[(]?\.[\+\*][)]?$/) { |
| |
335 &lint_warning($file, "", "", "wildcard not allowed for both ident and facility (found $ident/$facility"); |
| |
336 } |
| |
337 } |
| |
338 |
| |
339 # enforce a single space |
| |
340 if (length($ws1) != 1) { |
| |
341 &lint_warning($file, "", "", "whitespace count wrong between section ($section) and ident ($ident)"); |
| |
342 } |
| |
343 |
| |
344 # enforce a single space |
| |
345 if (length($ws2) != 1) { |
| |
346 &lint_warning($file, "", "", "whitespace count wrong between facility ($facility) and end of line"); |
| |
347 } |
| |
348 |
| |
349 # ident same as facility is likely to be a typo |
| |
350 if ($ident eq $facility) { |
| |
351 &lint_warning($file, "", "", "unusual constellation ident equal to facility (found $ident/$facility"); |
| |
352 } |
| |
353 |
| |
354 # FIXME MTAs hardcoded here for /mail |
| |
355 if ($facility eq "mail" and $pkg !~ m/^(sendmail|ssmtp|postfix|exim)$/) { |
| |
356 &lint_warning($file, "", "", "only MTAs may match facility mail"); |
| |
357 } |
| |
358 |
| |
359 # FIXME inn hardcoded here for /news |
| |
360 if ($facility eq "news" and $pkg !~ m/^(inn)$/) { |
| |
361 &lint_warning($file, "", "", "only inn may match facility news"); |
| |
362 } |
| |
363 |
| |
364 # check prefix channel |
| |
365 if ($body =~ m/\n([ ]*)prefix(\s*?)\((.*?)\)/s) { |
| |
366 my ($ws1, $ws2, $options) = ($1, $2, $3); |
| |
367 |
| |
368 # enforce eight spaces |
| |
369 if (length($ws1) != 4) { |
| |
370 &lint_warning($file, "", "", "prefix channel whitespace count at start of line"); |
| |
371 } |
| |
372 |
| |
373 # enforce zero spaces |
| |
374 if (length($ws2) != 0) { |
| |
375 &lint_warning($file, "", "", "whitespace not allowed between prefix channel and round open bracket"); |
| |
376 } |
| |
377 |
| |
378 # enforce prefix options in prefix channel |
| |
379 if ($options !~ m/\sprefix="%b %d %H:%M:%S %N (<%L> )?\$1(\[%P\])?: "/) { |
| |
380 &lint_warning($file, "", "", "prefix option in prefix channel invalid or missing"); |
| |
381 } |
| |
382 $options = $'; |
| |
383 $options =~ s/,//; |
| |
384 |
| |
385 # detect superflous options in prefix channel |
| |
386 if ($options =~ m/\S+/s) { |
| |
387 $options =~ s/\n/\\n/; |
| |
388 &lint_warning($file, "", "", "superflous option in prefix channel unseparated line detected: $options"); |
| |
389 } |
| |
390 } |
| |
391 else { |
| |
392 &lint_warning($file, "", "", "prefix channel missing"); |
| |
393 } |
| |
394 |
| |
395 # check path branch |
| |
396 if ($body !~ m/\n([ ]*)->(\s*?)\{(.*)\}\n/s) { |
| |
397 &lint_warning($file, "", "", "no path branch found"); |
| |
398 return; |
| |
399 } |
| |
400 my ($ws1, $ws2, $body) = ($1, $2, $3); #FIXME check ws1/ws2 |
| |
401 |
| |
402 # check path channel |
| |
403 while ($body =~ m/\n([ ]*)(\w+):(\s+?)file(\s*?)\((.*?)\);/s) { |
| |
404 my ($ws1, $level, $ws2, $ws3, $options) = ($1, $2, $3, $4, $5); |
| |
405 $body = $'; |
| |
406 |
| |
407 # enforce eight spaces |
| |
408 if (length($ws1) != 8) { |
| |
409 &lint_warning($file, "", "", "path channel whitespace count at start of line"); |
| |
410 } |
| |
411 |
| |
412 # enforce spaces |
| |
413 if (length($ws2) < 1) { |
| |
414 &lint_warning($file, "", "", "whitespace required between level and file"); |
| |
415 } |
| |
416 |
| |
417 # enforce zero spaces |
| |
418 if (length($ws3) != 0) { |
| |
419 &lint_warning($file, "", "", "path channel whitespace not allowed between file channel and round open bracket"); |
| |
420 } |
| |
421 |
| |
422 # check for legal l2 level |
| |
423 if ($level !~ m/^(panic|critical|error|warning|notice|info|trace|debug)$/) { |
| |
424 &lint_warning($file, "", "", "illegal l2 level $level detected"); |
| |
425 } |
| |
426 |
| |
427 # enforce file option in file channel |
| |
428 if ($options !~ m;path="\@l_prefix\@/var/$pkg/(log\S+|$pkg\.log)";) { |
| |
429 &lint_warning($file, "", "", "path option in file channel invalid or missing"); |
| |
430 } |
| |
431 $options = $'; |
| |
432 $options =~ s/,//; |
| |
433 |
| |
434 # enforce perm option in file channel |
| |
435 if ($options !~ m;perm=0[0-7]{3};) { |
| |
436 &lint_warning($file, "", "", "perm option in file channel invalid or missing"); |
| |
437 } |
| |
438 $options = $'; |
| |
439 $options =~ s/,//; |
| |
440 |
| |
441 # detect superflous options in file channel |
| |
442 if ($options =~ m/\S+/s) { |
| |
443 $options =~ s/\n/\\n/; |
| |
444 &lint_warning($file, "", "", "superflous option in prefix channel detected: $options"); |
| |
445 } |
| |
446 } |
| |
447 |
| |
448 # check path channel |
| |
449 if ($body =~ m/\n([ ]*)(\w+):(\s*?)file(\s*?)\((.*?)\)/s) { |
| |
450 my ($ws1, $level, $ws2, $ws3, $options) = ($1, $2, $3, $4, $5); |
| |
451 |
| |
452 # enforce eight spaces |
| |
453 if (length($ws1) != 8) { |
| |
454 &lint_warning($file, "", "", "path channel whitespace count at start of unseparated line"); |
| |
455 } |
| |
456 |
| |
457 # enforce spaces |
| |
458 if (length($ws2) < 1) { |
| |
459 &lint_warning($file, "", "", "path channel whitespace required between level and file of unseparated line"); |
| |
460 } |
| |
461 |
| |
462 # enforce zero spaces |
| |
463 if (length($ws3) != 0) { |
| |
464 &lint_warning($file, "", "", "whitespace not allowed between file channel and round open bracket"); |
| |
465 } |
| |
466 |
| |
467 # check for legal l2 level |
| |
468 if ($level !~ m/^(panic|critical|error|warning|notice|info|trace|debug)$/) { |
| |
469 &lint_warning($file, "", "", "illegal l2 level $level detected on unseparated line"); |
| |
470 } |
| |
471 |
| |
472 # enforce file option in file channel |
| |
473 if ($options !~ m;path="\@l_prefix\@/var/$pkg/(log\S+|$pkg\.log)";) { |
| |
474 &lint_warning($file, "", "", "XXX path option in file channel invalid or missing on unseparated line"); |
| |
475 } |
| |
476 $options = $'; |
| |
477 $options =~ s/,//; |
| |
478 |
| |
479 # enforce perm option in file channel |
| |
480 if ($options !~ m;perm=0[0-7]{3};) { |
| |
481 &lint_warning($file, "", "", "perm option in file channel invalid or missing on unseparated line"); |
| |
482 } |
| |
483 |
| |
484 $options = $'; |
| |
485 $options =~ s/, jitter=[0-9]+//; |
| |
486 $options =~ s/, monitor=[0-9]+//; |
| |
487 $options =~ s/,//; |
| |
488 |
| |
489 # detect superflous options in file channel |
| |
490 if ($options =~ m/\S+/s) { |
| |
491 $options =~ s/\n/\\n/; |
| |
492 &lint_warning($file, "", "", "superflous option in file channel unseparated line detected: $options"); |
| |
493 } |
| |
494 } |
| |
495 else { |
| |
496 &lint_warning($file, "", "", "file channel missing"); |
| |
497 } |
| |
498 |
| |
499 $done .= $this; |
| |
500 } |
| |
501 return; |
| |
502 } |