1 # menus -- lintian check script -*- perl -*-
3 # somewhat of a misnomer -- it doesn't only check menus
5 # Copyright (C) 1998 Christian Schwarz
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, you can find it on the World Wide
19 # Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
20 # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
23 package Lintian::menus;
25 use lib "$ENV{'LINTIAN_ROOT'}/checks/";
37 # Known fields for doc-base files. The value is 1 for required fields and 0
38 # for optional fields.
39 my %known_docbase_main_fields = (
46 my %known_docbase_format_fields = (
67 # check preinst script
68 if ( -f "control/preinst" ) {
70 check_script("preinst",\%preinst);
73 # check postinst script
74 if ( -f "control/postinst" ) {
76 check_script("postinst",\%postinst);
80 if ( -f "control/prerm" ) {
82 check_script("prerm",\%prerm);
86 if ( -f "control/postrm" ) {
88 check_script("postrm",\%postrm);
91 # read package contents
92 open(IN, '<', "index") or fail("cannot open index file index: $!");
95 my ($perm,$owner,$size,$date,$time,$file) = split(' ', $_, 6);
97 add_file_link_info ($file);
98 $file =~ s/ link to .*//;
101 my $operm = perm2oct($perm);
103 if ($perm =~ m,^-,o) { # file checks
105 if ($file =~ m,^usr/(lib|share)/menu/\S,o) { # correct permissions?
106 if ($perm =~ m,x,o) {
107 tag "executable-menu-file", sprintf("$file %04o",$operm);
110 next if $file =~ m,^usr/(lib|share)/menu/README$,;
112 if ($file =~ m,^usr/lib/,o) {
113 tag "menu-file-in-usr-lib", $file;
118 if ($file =~ m,usr/(lib|share)/menu/menu$,o and $pkg ne 'menu') {
119 tag "bad-menu-file-name", $file;
123 elsif ($file =~ m,^usr/share/doc-base/\S,o) { # correct permissions?
124 if ($perm =~ m,x,o) {
125 tag "executable-in-usr-share-docbase", $file, sprintf("%04o",$operm);
127 $docbase_file = $file;
130 elsif ( $file =~ m,^etc/menu-methods/\S,o ) {
131 #TODO: we should test if the menu-methods file
132 # is made executable in the postinst as recommended by
135 my $menumethod_includes_menu_h = 0;
136 $menumethod_file = $file;
138 open(MM, '<', "unpacked/$file") or fail("cannot open menu-method file $file: $!");
141 if (m,^!include menu.h,o) {
142 $menumethod_includes_menu_h = 1;
147 tag "menu-method-should-include-menu-h", "$file"
148 unless $menumethod_includes_menu_h or $pkg eq 'menu';
154 # prerm scripts should not call update-menus
155 if ($prerm{'calls-updatemenus'}) {
156 tag "prerm-calls-updatemenus", "";
159 # postrm scripts should not call install-docs
160 if ($postrm{'calls-installdocs'} or $postrm{'calls-installdocs-r'}) {
161 tag "postrm-calls-installdocs", "";
164 # preinst scripts should not call either update-menus nor installdocs
165 if ($preinst{'calls-updatemenus'}) {
166 tag "preinst-calls-updatemenus", "";
169 if ($preinst{'calls-installdocs'}) {
170 tag "preinst-calls-installdocs", "";
173 # don't set the /usr/doc link, the FHS transition is over (2002-10-08)
174 if (defined $postinst{'sets-link'} && $postinst{'sets-link'} == 1) {
175 tag "postinst-should-not-set-usr-doc-link", "";
178 $anymenu_file = $menu_file || $menumethod_file;
182 if ($docbase_file) { # postinst has to call install-docs
183 if (not $postinst{'calls-installdocs'}) {
184 tag "postinst-does-not-call-installdocs", "$docbase_file";
186 # prerm has to call install-docs -r
187 if (not $prerm{'calls-installdocs-r'}) {
188 tag "prerm-does-not-call-installdocs", "$docbase_file";
191 # check the contents of the doc-base file(s)
192 opendir DOCBASEDIR, "doc-base" or fail("cannot read doc-base directory.");
194 while (defined ($dbfile = readdir DOCBASEDIR)) {
195 # don't try to parse executables, plus we already warned about it
196 next if -x "doc-base/$dbfile";
197 check_doc_base_file($dbfile, $pkg, $type);
201 # postinst and postrm should not need to call install-docs
202 if ($postinst{'calls-installdocs'} or $postinst{'calls-installdocs-r'}) {
203 tag "postinst-has-useless-call-to-install-docs", "";
205 if ($prerm{'calls-installdocs'} or $prerm{'calls-installdocs-r'}) {
206 tag "prerm-has-useless-call-to-install-docs", "";
211 # postinst has to call update-menus
212 if (not $postinst{'calls-updatemenus'}) {
213 tag "postinst-does-not-call-updatemenus", "$anymenu_file";
215 # postrm has to call update-menus
216 if (not $postrm{'calls-updatemenus'}) {
217 tag "postrm-does-not-call-updatemenus", "$anymenu_file" unless $pkg eq 'menu';
220 # postinst and postrm should not need to call update-menus
221 if ($postinst{'calls-updatemenus'}) {
222 tag "postinst-has-useless-call-to-update-menus", "";
224 if ($postrm{'calls-updatemenus'}) {
225 tag "postrm-has-useless-call-to-update-menus", "";
231 # -----------------------------------
233 sub check_doc_base_file {
234 my ($dbfile, $pkg, $type) = @_;
236 my $line = file_is_encoded_in_non_utf8("doc-base/$dbfile", $type, $pkg);
238 tag 'doc-base-file-uses-obsolete-national-encoding', "$dbfile:$line";
241 open (IN, '<', "doc-base/$dbfile")
242 or fail("cannot open doc-base file $dbfile for reading.");
244 my (@files, $field, @vals);
245 my $knownfields = \%known_docbase_main_fields;
247 my %sawfields = (); # local for each section of control file
248 my %sawformats = (); # global for control file
253 # New field. check previous field, if we have any.
254 if (/^(\S+)\s*:\s*(.*)$/) {
255 my (@new) = ($1, $2);
257 check_doc_base_field($dbfile, $line, $field, \@vals,
258 \%sawfields, \%sawformats, $knownfields);
264 # Continuation of previously defined field.
265 } elsif ($field && /^\s+\S/) {
268 # All tags will be reported on the last continuation line of the
272 # Sections' separator.
273 } elsif (/^(\s*)$/) {
274 tag "doc-base-file-separator-extra-whitespaces", "$dbfile:$."
276 next unless $field; # skip successive empty lines
278 # Check previously defined field and section.
279 check_doc_base_field($dbfile, $line, $field, \@vals, \%sawfields,
280 \%sawformats, $knownfields);
281 check_doc_base_file_section($dbfile, $line + 1, \%sawfields,
282 \%sawformats, $knownfields);
284 # Intialize variables for new section.
290 # Each section except the first one is format section.
291 $knownfields = \%known_docbase_format_fields;
293 # Everything else is a syntax error.
295 tag "doc-base-file-syntax-error", "$dbfile:$.";
299 # Check the last field/section of the control file.
301 check_doc_base_field($dbfile, $line, $field, \@vals, \%sawfields,
302 \%sawformats, $knownfields);
303 check_doc_base_file_section($dbfile, $line, \%sawfields, \%sawformats,
307 # Make sure we saw at least one format.
308 tag "doc-base-file-no-format-section", "$dbfile:$." unless %sawformats;
313 # Checks one field of a doc-base control file. $vals is array ref containing
314 # all lines of the field. Modifies $sawfields and $sawformats.
315 sub check_doc_base_field {
316 my ($dbfile, $line, $field, $vals, $sawfields, $sawformats,
319 tag "doc-base-file-unknown-field", "$dbfile:$line", "$field"
320 unless defined $knownfields->{$field};
321 tag "doc-base-file-duplicated-field", "$dbfile:$line", "$field"
322 if $sawfields->{$field};
323 $sawfields->{$field} = 1;
327 # Check if files referenced by doc-base are included in the package. The
328 # Index field should refer to only one file without wildcards. The Files
329 # field is a whitespace-separated list of files and may contain wildcards.
330 # We skip without validating wildcard patterns containing character
331 # classes since otherwise we'd need to deal with wildcards inside
332 # character classes and aren't there yet.
333 if ($field eq 'index' or $field eq 'files') {
334 my @files = map { split ('\s+', $_) } @$vals;
336 if ($field eq 'index' && @files > 1) {
337 tag "doc-base-index-references-multiple-files", "$dbfile:$line";
339 for my $file (@files) {
340 if ($file =~ m%^/usr/doc%) {
341 tag "doc-base-file-references-usr-doc", "$dbfile:$line";
343 my $realfile = delink ($file);
345 # openoffice.org-dev-doc has thousands of files listed so try to
346 # use the hash if possible.
348 if ($realfile =~ /[*?]/) {
349 my $regex = quotemeta ($realfile);
350 unless ($field eq 'index') {
351 next if $regex =~ /\[/;
352 $regex =~ s%\\\*%[^/]*%g;
353 $regex =~ s%\\\?%[^/]%g;
356 $found = grep { /^$regex\z/ } keys %all_files;
358 $found = $all_files{$realfile} || $all_files{"$realfile/"};
361 tag "doc-base-file-references-missing-file", "$dbfile:$line",
368 } elsif ($field eq 'format') {
369 my $format = join (' ', @$vals);
370 $format =~ s/^\s+//o;
371 $format =~ s/\s+$//o;
372 $format = lc $format;
373 tag "doc-base-file-unknown-format", "$dbfile:$line", $format
374 unless $known_doc_base_formats{$format};
375 tag "doc-base-file-duplicated-format", "$dbfile:$line", $format
376 if $sawformats->{$format};
377 $sawformats->{$format} = 1;
379 # Save the current format for the later section check.
380 $sawformats->{' *current* '} = $format;
383 } elsif ($field eq 'document') {
384 $_ = join (' ', @$vals);
386 tag "doc-base-invalid-document-field", "$dbfile:$line", "$_"
387 unless /^[a-z0-9+.-]+$/;
388 tag "doc-base-document-field-ends-in-whitespace", "$dbfile:$line"
390 tag "doc-base-document-field-not-in-first-line", "$dbfile:$line"
394 } elsif ($field eq 'title') {
396 spelling_check("spelling-error-in-doc-base-title-field",
397 join (' ', @$vals), "$dbfile:$line");
398 spelling_check_picky("spelling-error-in-doc-base-title-field",
399 join (' ', @$vals), "$dbfile:$line");
403 } elsif ($field eq 'section') {
404 my $sections = Lintian::Data->new('doc-base/sections');
405 $_ = join (' ', @$vals);
406 tag "doc-base-unknown-section", "$dbfile:$line", $_
407 unless $sections->known($_);
410 } elsif ($field eq 'abstract') {
411 # The three following variables are used for checking if the field is
412 # correctly phrased. We detect if each line (except for the first
413 # line and lines containing single dot) of the field starts with the
414 # same number of spaces, not followed by the same non-space character,
415 # and the number of spaces is > 1.
417 # We try to match fields like this:
418 # ||Abstract: The Boost web site provides free peer-reviewed portable
419 # || C++ source libraries. The emphasis is on libraries which work
420 # || well with the C++ Standard Library. One goal is to establish
423 # ||Abstract: This is "Ding"
424 # || * a dictionary lookup program for Unix,
425 # || * DIctionary Nice Grep,
426 my $leadsp = undef; # string with leading spaces from second line
427 my $charafter = undef; # first non-whitespace char of second line
428 my $leadsp_ok = 1; # are spaces OK?
430 # Intentionally skipping the first line.
431 for my $idx (1 .. $#{$vals}) {
433 if (/manage\s+online\s+manuals\s.*Debian/o) {
434 tag "doc-base-abstract-field-is-template", "$dbfile:$line"
435 unless $pkg eq "doc-base";
436 } elsif (/^(\s+)\.(\s*)$/o and ($1 ne " " or $2)) {
437 tag "doc-base-abstract-field-separator-extra-whitespaces",
438 "$dbfile:" . ($line - $#{$vals} + $idx);
439 } elsif (!$leadsp && /^(\s+)(\S)/o) {
440 # The regexp should always match.
441 ($leadsp, $charafter) = ($1, $2);
442 $leadsp_ok = $leadsp eq " ";
443 } elsif (!$leadsp_ok && /^(\s+)(\S)/o) {
444 # The regexp should always match.
445 undef $charafter if $charafter && $charafter ne $2;
447 if ($1 ne $leadsp) || ($1 eq $leadsp && $charafter);
450 unless ($leadsp_ok) {
451 tag "doc-base-abstract-might-contain-extra-leading-whitespaces",
457 spelling_check("spelling-error-in-doc-base-abstract-field",
458 join (' ', @$vals), "$dbfile:$line");
459 spelling_check_picky("spelling-error-in-doc-base-abstract-field",
460 join (' ', @$vals), "$dbfile:$line");
465 # Checks the section of the doc-base control file. Tries to find required
466 # fields missing in the section.
467 sub check_doc_base_file_section {
468 my ($dbfile, $line, $sawfields, $sawformats, $knownfields) = @_;
470 tag "doc-base-file-no-format", "$dbfile:$line"
471 if ((defined $sawfields->{'files'} || defined $sawfields->{'index'})
472 && !(defined $sawfields->{'format'}));
474 # The current format is set by check_doc_base_field.
475 if ($sawfields->{'format'}) {
476 my $format = $sawformats->{' *current* '};
477 tag "doc-base-file-no-index", "$dbfile:$line"
478 if ($format && ($format eq 'html' || $format eq 'info')
479 && !$sawfields->{'index'});
481 for my $field (sort keys %$knownfields) {
482 tag "doc-base-file-lacks-required-field", "$dbfile:$line", "$field"
483 if ($knownfields->{$field} == 1 && !$sawfields->{$field});
487 # Add file and link to %all_files and %all_links. Note that both files and
488 # links have to include a leading /.
489 sub add_file_link_info {
493 $file = "/" . $file if (not $file =~ m%^/%); # make file absolute
494 $file =~ s%/+%/%g; # remove duplicated `/'
495 my $ishard = ($file =~ / link to /);
496 ($file, $link) = split(/ (?:->|link to) /, $file);
498 $all_files{$file} = 1;
503 } elsif (not $link =~ m,^/,) { # not absolute link
504 $link = "/" . $link; # make sure link starts with '/'
505 $link =~ s,/+\./+,/,g; # remove all /./ parts
507 while ($link =~ s,^/+\.\./+,/,) { #\ count & remove
508 $dcount++; #/ any leading /../ parts
511 while ($dcount--) { #\ remove last $dcount
512 $f =~ s,/[^/]*$,,; #/ path components from $file
514 $link = $f . $link; # now we should have absolute link
516 $all_links{$file} = $link unless ($link eq $file);
521 # Dereference all symlinks in file. Uses %all_links.
525 $file =~ s%/+%/%g; # remove duplicated '/'
526 return $file unless %all_links; # package doesn't symlinks
532 # In the loop below we split $file into two parts on each '/' until
533 # there's no remaining slashes. We try substituting the first part with
534 # corresponding symlink and if it succeedes, we start the procedure from
538 # Let $all_links{"/a/b"} == "/d", and $file == "/a/b/c"
539 # Then 0) $p1 == "", $p2 == "/a/b/c"
540 # 1) $p1 == "/a", $p2 == "/b/c"
541 # 2) $p1 == "/a/b", $p2 == "/c" ; substitute "/d" for "/a/b"
542 # 3) $p1 == "", $p2 == "/d/c"
543 # 4) $p1 == "/d", $p2 == "/c"
544 # 5) $p1 == "/d/c", $p2 == ""
546 # Note that the algorithm supposes, that
547 # i) $all_links{$X} != $X for each $X
548 # ii) both keys and values of %all_links start with '/'
550 while (($p2 =~ s%^(/[^/]*)%%g) > 0) {
552 if (defined $all_links{$p1}) {
553 return '!!! SYMLINK LOOP !!!' if defined $used_links{$p1};
554 $p2 = $all_links{$p1} . $p2;
556 $used_links{$p1} = 1;
560 # After the loop $p2 should be empty and $p1 should contain the target
561 # file. In some rare cases when $file contains no slashes, $p1 will be
562 # empty and $p2 will contain the result (which will be equal to $file).
563 return $p1 ne "" ? $p1 : $p2;
567 my ($script,$pres) = @_;
568 my ($no_check_menu,$no_check_installdocs,$no_check_wmmenu,$calls_wmmenu);
571 open(IN, '<', "control/$script") or
572 fail("cannot open maintainer script control/$script for reading: $!");
574 $interp = '' unless defined $interp;
575 if ($interp =~ m,^\#\!\s*/bin/(a|ba|k|pdk)?sh,) {
577 } elsif ($interp =~ m,^\#\!\s*/usr/bin/perl,) {
580 if ($interp =~ m,^\#\!\s*(.+),) {
583 else { # hmm, doesn't seem to start with #!
584 # is it a binary? look for ELF header
585 if ($interp =~ m/^\177ELF/) {
586 return; # nothing to do here
597 # either update-menus or wm-menu-config will satisfy
598 # the checks that the menu file installed is properly used
601 # does the script check whether update-menus exists?
602 if (/-x\s+\S*update-menus/o or /(which|command)(\s+\S+)?\s+update-menus/o) {
604 $pres->{'checks-for-updatemenus'} = 1;
607 # does the script call update-menus?
608 # TODO this regex-magic should be moved to some lib for checking
609 # whether a certain word is likely called as command... --Jeroen
610 if (/(?:^\s*|[;&|]\s*|(?:then|do)\s+)(?:\/usr\/bin\/)?update-menus(?:\s|[;&|<>]|$)/) {
612 $pres->{'calls-updatemenus'} = 1;
615 if (not $pres->{'checks-for-updatemenus'} and $pkg ne 'menu') {
616 tag "maintainer-script-does-not-check-for-existence-of-updatemenus", "$script:$." unless $no_check_menu++;
620 # does the script check whether wm-menu-config exists?
621 if (s/-x\s+\S*wm-menu-config//o or /which\s+wm-menu-config/o
622 or s/command\s+.*?wm-menu-config//o) {
624 $pres->{'checks-for-wmmenuconfig'} = 1;
627 # does the script call wm-menu-config?
628 if (m/(?:^\s*|[;&|]\s*|(?:then|do)\s+)(?:\/usr\/sbin\/)?wm-menu-config(?:\s|[;&|<>]|$)/) {
630 $pres->{'calls-wmmenuconfig'} = 1;
631 tag "maintainer-script-calls-deprecated-wm-menu-config", "$script:$." unless $calls_wmmenu++;
634 if (not $pres->{'checks-for-wmmenuconfig'} and $pkg ne 'menu') {
635 tag "maintainer-script-does-not-check-for-existence-of-wm-menu-config", "$script:$." unless $no_check_wmmenu++;
639 # does the script set a link in /usr/doc?
640 # does the script remove a link in /usr/doc?
641 if ($interp eq 'sh') {
642 if (m,ln\s+(-\w+)?\s+\"?\.\./share/doc/\S+, ) {
643 $pres->{'sets-link'} = 1;
645 if (m,rm\s+(-\w+\s+)?\"?/usr/doc/\S+, ) {
646 $pres->{'removes-link'} = 1;
648 } elsif ($interp eq 'perl') {
649 if (m|symlink\s*\(?\s*[\"\']\.\./share/doc/\.+?[\"\']\s*,|) {
650 $pres->{'sets-link'} = 1;
651 } elsif (m,ln\s+(-\w+)?\s+\"?\.\./share/doc/\S+, ) {
652 $pres->{'sets-link'} = 1;
655 # just fall through for now
658 # does the script check whether install-docs exists?
659 if (s/-x\s+\S*install-docs//o or /which\s+install-docs/o
660 or s/command\s+.*?install-docs//o) {
662 $pres->{'checks-for-installdocs'} = 1;
665 # does the script call install-docs?
666 if (m/(?:^\s*|[;&|]\s*|(?:then|do)\s+)(?:\/usr\/sbin\/)?install-docs(?:\s|[;&|<>]|$)/) {
667 # yes, it does. Does it remove or add a doc?
668 if (m/install-docs\s+(-r|--remove)\s/) {
669 $pres->{'calls-installdocs-r'} = 1;
671 $pres->{'calls-installdocs'} = 1;
674 if (not $pres->{'checks-for-installdocs'}) {
675 tag "maintainer-script-does-not-check-for-existence-of-installdocs", "$script" unless $no_check_installdocs++;
685 # indent-tabs-mode: t
686 # cperl-indent-level: 4