# menus -- lintian check script -*- perl -*-

# somewhat of a misnomer -- it doesn't only check menus

# Copyright (C) 1998 Christian Schwarz
# Copyright (C) 2018 Chris Lamb <lamby@debian.org>
# Copyright (C) 2021 Felix Lechner
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, you can find it on the World Wide
# Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.

package Lintian::Check::Menus;

use v5.20;
use warnings;
use utf8;

use Const::Fast;
use Path::Tiny;
use Unicode::UTF8 qw(encode_utf8);

use Lintian::Spelling qw(check_spelling check_spelling_picky);

use Moo;
use namespace::clean;

with 'Lintian::Check';

const my $EMPTY => q{};
const my $SPACE => q{ };
const my $SLASH => q{/};
const my $DOT => q{.};
const my $QUESTION_MARK => q{?};

# Supported documentation formats for doc-base files.
my %known_doc_base_formats
  = map { $_ => 1 }qw(html text pdf postscript info dvi debiandoc-sgml);

# Known fields for doc-base files.  The value is 1 for required fields and 0
# for optional fields.
my %KNOWN_DOCBASE_MAIN_FIELDS = (
    'Document' => 1,
    'Title'    => 1,
    'Section'  => 1,
    'Abstract' => 0,
    'Author'   => 0
);

my %KNOWN_DOCBASE_FORMAT_FIELDS = (
    'Format'  => 1,
    'Files'   => 1,
    'Index'   => 0
);

has menu_item => (is => 'rw');
has menumethod_item => (is => 'rw');
has documentation => (is => 'rw', default => 0);

sub spelling_tag_emitter {
    my ($self, @orig_args) = @_;

    return sub {
        return $self->pointed_hint(@orig_args, @_);
    };
}

sub visit_installed_files {
    my ($self, $item) = @_;

    if ($item->is_file) { # file checks
         # menu file?
        if ($item->name =~ m{^usr/(lib|share)/menu/\S}){ # correct permissions?

            $self->pointed_hint('executable-menu-file', $item->pointer,
                $item->octal_permissions)
              if $item->is_executable;

            return
              if $item->name =~ m{^usr/(?:lib|share)/menu/README$};

            if ($item->name =~ m{^usr/lib/}) {
                $self->pointed_hint('menu-file-in-usr-lib', $item->pointer);
            }

            $self->menu_item($item);

            $self->pointed_hint('bad-menu-file-name', $item->pointer)
              if $item->name =~ m{^usr/(?:lib|share)/menu/menu$}
              && $self->processable->name ne 'menu';
        }
        #menu-methods file?
        elsif ($item->name =~ m{^etc/menu-methods/\S}) {
            #TODO: we should test if the menu-methods file
            # is made executable in the postinst as recommended by
            # the menu manual

            my $menumethod_includes_menu_h = 0;
            $self->menumethod_item($item);

            if ($item->is_open_ok) {
                open(my $fd, '<', $item->unpacked_path)
                  or die encode_utf8('Cannot open ' . $item->unpacked_path);

                while (my $line = <$fd>) {
                    chomp $line;
                    if ($line =~ /^!include menu.h/) {
                        $menumethod_includes_menu_h = 1;
                        last;
                    }
                }
                close($fd);
            }

            $self->pointed_hint('menu-method-lacks-include', $item->pointer)
              unless $menumethod_includes_menu_h
              or $self->processable->name eq 'menu';
        }
        # package doc dir?
        elsif (
            $item->name =~ m{ \A usr/share/doc/(?:[^/]+/)?
                                 (.+\.(?:html|pdf))(?:\.gz)?
                          \Z}xsm
        ) {
            my $name = $1;
            unless ($name =~ m/^changelog\.html$/
                or $name =~ m/^README[.-]/
                or $name =~ m/examples/) {
                $self->documentation(1);
            }
        }
    }

    return;
}

sub installable {
    my ($self) = @_;

    my $pkg = $self->processable->name;
    my $processable = $self->processable;
    my $group = $self->group;

    my (%all_files, %all_links);

    my %preinst;
    my %postinst;
    my %prerm;
    my %postrm;

    $self->check_script($processable->control->lookup('preinst'),\%preinst);
    $self->check_script($processable->control->lookup('postinst'),\%postinst);
    $self->check_script($processable->control->lookup('prerm'),\%prerm);
    $self->check_script($processable->control->lookup('postrm'),\%postrm);

    # Populate all_{files,links} from current package and its dependencies
    for my $installable ($group->get_installables) {
        next
          unless $processable->name eq $installable->name
          || $processable->relation('strong')->satisfies($installable->name);

        for my $item (@{$installable->installed->sorted_list}) {
            add_file_link_info($installable, $item->name, \%all_files,
                \%all_links);
        }
    }

    # prerm scripts should not call update-menus
    $self->pointed_hint('prerm-calls-updatemenus',$prerm{'calls-updatemenus'})
      if defined $prerm{'calls-updatemenus'};

    # postrm scripts should not call install-docs
    $self->pointed_hint('postrm-calls-installdocs',
        $postrm{'calls-installdocs'})
      if defined $postrm{'calls-installdocs'};
    $self->pointed_hint('postrm-calls-installdocs',
        $postrm{'calls-installdocs-r'})
      if defined $postrm{'calls-installdocs-r'};

    # preinst scripts should not call either update-menus nor installdocs
    $self->pointed_hint('preinst-calls-updatemenus',
        $preinst{'calls-updatemenus'})
      if defined $preinst{'calls-updatemenus'};

    $self->pointed_hint('preinst-calls-installdocs',
        $preinst{'calls-installdocs'})
      if defined $preinst{'calls-installdocs'};

    my $anymenu_item = $self->menu_item || $self->menumethod_item;

    # No one needs to call install-docs any more; triggers now handles that.
    $self->pointed_hint('postinst-has-useless-call-to-install-docs',
        $postinst{'calls-installdocs'})
      if defined $postinst{'calls-installdocs'};
    $self->pointed_hint('postinst-has-useless-call-to-install-docs',
        $postinst{'calls-installdocs-r'})
      if defined $postinst{'calls-installdocs-r'};

    $self->pointed_hint('prerm-has-useless-call-to-install-docs',
        $prerm{'calls-installdocs'})
      if defined $prerm{'calls-installdocs'};
    $self->pointed_hint('prerm-has-useless-call-to-install-docs',
        $prerm{'calls-installdocs-r'})
      if defined $prerm{'calls-installdocs-r'};

    # check consistency
    # docbase file?
    if (my $db_dir
        = $processable->installed->resolve_path('usr/share/doc-base/')){
        for my $item ($db_dir->children) {
            next
              if !$item->is_open_ok;

            if ($item->resolve_path->is_executable) {

                $self->pointed_hint('executable-in-usr-share-docbase',
                    $item->pointer, $item->octal_permissions);
                next;
            }

            $self->check_doc_base_file($item, \%all_files,\%all_links);
        }
    } elsif ($self->documentation) {
        if ($pkg =~ /^libghc6?-.*-doc$/) {
            # This is the library documentation for a haskell library. Haskell
            # libraries register their documentation via the ghc compiler's
            # documentation registration mechanism.  See bug #586877.
        } else {
            $self->hint('possible-documentation-but-no-doc-base-registration');
        }
    }

    if ($anymenu_item) {
        # postinst and postrm should not need to call update-menus
        # unless there is a menu-method file.  However, update-menus
        # currently won't enable packages that have outstanding
        # triggers, leading to an update-menus call being required for
        # at least some packages right now.  Until this bug is fixed,
        # we still require it.  See #518919 for more information.
        #
        # That bug does not require calling update-menus from postrm,
        # but debhelper apparently currently still adds that to the
        # maintainer script, so don't warn if it's done.
        $self->pointed_hint('postinst-does-not-call-updatemenus',
            $anymenu_item->pointer)
          if !defined $postinst{'calls-updatemenus'};

        $self->pointed_hint(
            'postrm-does-not-call-updatemenus',
            $self->menumethod_item->pointer
          )
          if defined $self->menumethod_item
          && !defined $postrm{'calls-updatemenus'}
          && $pkg ne 'menu';

    } else {
        $self->pointed_hint('postinst-has-useless-call-to-update-menus',
            $postinst{'calls-updatemenus'})
          if defined $postinst{'calls-updatemenus'};

        $self->pointed_hint('postrm-has-useless-call-to-update-menus',
            $postrm{'calls-updatemenus'})
          if defined $postrm{'calls-updatemenus'};
    }

    return;
}

# -----------------------------------

sub check_doc_base_file {
    my ($self, $item, $all_files, $all_links) = @_;

    my $pkg = $self->processable->name;
    my $group = $self->group;

    # another check complains about invalid encoding
    return
      unless ($item->is_valid_utf8);

    my $contents = $item->decoded_utf8;
    my @lines = split(/\n/, $contents);

    my $knownfields = \%KNOWN_DOCBASE_MAIN_FIELDS;
    my ($field, @vals);
    my %sawfields;        # local for each section of control file
    my %sawformats;       # global for control file
    my $line           = 0;  # global

    my $position = 1;
    while (defined(my $string = shift @lines)) {
        chomp $string;

        # New field.  check previous field, if we have any.
        if ($string =~ /^(\S+)\s*:\s*(.*)$/) {
            my (@new) = ($1, $2);
            if ($field) {
                $self->check_doc_base_field(
                    $item, $line, $field,
                    \@vals,\%sawfields, \%sawformats,
                    $knownfields,$all_files, $all_links
                );
            }

            $field = $new[0];

            @vals  = ($new[1]);
            $line  = $position;

            # Continuation of previously defined field.
        } elsif ($field && $string =~ /^\s+\S/) {
            push(@vals, $string);

            # All tags will be reported on the last continuation line of the
            # doc-base field.
            $line  = $position;

            # Sections' separator.
        } elsif ($string =~ /^(\s*)$/) {
            $self->pointed_hint('doc-base-file-separator-extra-whitespace',
                $item->pointer($position))
              if $1;
            next unless $field; # skip successive empty lines

            # Check previously defined field and section.
            $self->check_doc_base_field(
                $item, $line, $field,
                \@vals,\%sawfields, \%sawformats,
                $knownfields,$all_files, $all_links
            );
            $self->check_doc_base_file_section($item, $line + 1,\%sawfields,
                \%sawformats, $knownfields);

            # Initialize variables for new section.
            undef $field;
            undef $line;
            @vals      = ();
            %sawfields = ();

            # Each section except the first one is format section.
            $knownfields = \%KNOWN_DOCBASE_FORMAT_FIELDS;

            # Everything else is a syntax error.
        } else {
            $self->pointed_hint('doc-base-file-syntax-error',
                $item->pointer($position));
        }

    } continue {
        ++$position;
    }

    # Check the last field/section of the control file.
    if ($field) {
        $self->check_doc_base_field(
            $item, $line, $field,
            \@vals, \%sawfields,\%sawformats,
            $knownfields,$all_files,$all_links
        );
        $self->check_doc_base_file_section($item, $line, \%sawfields,
            \%sawformats,$knownfields);
    }

    # Make sure we saw at least one format.
    $self->pointed_hint('doc-base-file-no-format-section', $item->pointer)
      unless %sawformats;

    return;
}

# Checks one field of a doc-base control file.  $vals is array ref containing
# all lines of the field.  Modifies $sawfields and $sawformats.
sub check_doc_base_field {
    my (
        $self, $item, $position, $field,$vals,
        $sawfields, $sawformats,$knownfields,$all_files, $all_links
    ) = @_;

    my $pkg = $self->processable->name;
    my $group = $self->group;

    my $SECTIONS = $self->data->load('doc-base/sections');

    $self->pointed_hint('doc-base-file-unknown-field',
        $item->pointer($position), $field)
      unless defined $knownfields->{$field};
    $self->pointed_hint('duplicate-field-in-doc-base',
        $item->pointer($position), $field)
      if $sawfields->{$field};
    $sawfields->{$field} = 1;

    # Index/Files field.
    #
    # Check if files referenced by doc-base are included in the package.  The
    # Index field should refer to only one file without wildcards.  The Files
    # field is a whitespace-separated list of files and may contain wildcards.
    # We skip without validating wildcard patterns containing character
    # classes since otherwise we'd need to deal with wildcards inside
    # character classes and aren't there yet.
    if ($field eq 'Index' or $field eq 'Files') {
        my @files = map { split($SPACE) } @{$vals};

        if ($field eq 'Index' && @files > 1) {
            $self->pointed_hint('doc-base-index-references-multiple-files',
                $item->pointer($position));
        }
        for my $file (@files) {
            next if $file =~ m{^/usr/share/doc/};
            next if $file =~ m{^/usr/share/info/};

            $self->pointed_hint('doc-base-file-references-wrong-path',
                $item->pointer($position), $file);
        }
        for my $file (@files) {
            my $realfile = delink($file, $all_links);
            # openoffice.org-dev-doc has thousands of files listed so try to
            # use the hash if possible.
            my $found;
            if ($realfile =~ /[*?]/) {
                my $regex = quotemeta($realfile);
                unless ($field eq 'Index') {
                    next if $regex =~ /\[/;
                    $regex =~ s{\\\*}{[^/]*}g;
                    $regex =~ s{\\\?}{[^/]}g;
                    $regex .= $SLASH . $QUESTION_MARK;
                }
                $found = grep { /^$regex\z/ } keys %{$all_files};
            } else {
                $found = $all_files->{$realfile} || $all_files->{"$realfile/"};
            }
            unless ($found) {
                $self->pointed_hint('doc-base-file-references-missing-file',
                    $item->pointer($position),$file);
            }
        }
        undef @files;

        # Format field.
    } elsif ($field eq 'Format') {
        my $format = join($SPACE, @{$vals});

        # trim both ends
        $format =~ s/^\s+|\s+$//g;

        $format = lc $format;
        $self->pointed_hint('doc-base-file-unknown-format',
            $item->pointer($position), $format)
          unless $known_doc_base_formats{$format};
        $self->pointed_hint('duplicate-format-in-doc-base',
            $item->pointer($position), $format)
          if $sawformats->{$format};
        $sawformats->{$format} = 1;

        # Save the current format for the later section check.
        $sawformats->{' *current* '} = $format;

        # Document field.
    } elsif ($field eq 'Document') {
        $_ = join($SPACE, @{$vals});

        $self->pointed_hint('doc-base-invalid-document-field',
            $item->pointer($position), $_)
          unless /^[a-z0-9+.-]+$/;
        $self->pointed_hint('doc-base-document-field-ends-in-whitespace',
            $item->pointer($position))
          if /[ \t]$/;
        $self->pointed_hint('doc-base-document-field-not-in-first-line',
            $item->pointer($position))
          unless $position == 1;

        # Title field.
    } elsif ($field eq 'Title') {
        if (@{$vals}) {
            my $stag_emitter
              = $self->spelling_tag_emitter(
                'spelling-error-in-doc-base-title-field',
                $item->pointer($position));
            check_spelling(
                $self->data,
                join($SPACE, @{$vals}),
                $group->spelling_exceptions,
                $stag_emitter
            );
            check_spelling_picky($self->data, join($SPACE, @{$vals}),
                $stag_emitter);
        }

        # Section field.
    } elsif ($field eq 'Section') {
        $_ = join($SPACE, @{$vals});
        unless ($SECTIONS->recognizes($_)) {
            if (m{^App(?:lication)?s/(.+)$} && $SECTIONS->recognizes($1)) {
                $self->pointed_hint('doc-base-uses-applications-section',
                    $item->pointer($position), $_);
            } elsif (m{^(.+)/(?:[^/]+)$} && $SECTIONS->recognizes($1)) {
                # allows creating a new subsection to a known section
            } else {
                $self->pointed_hint('doc-base-unknown-section',
                    $item->pointer($position), $_);
            }
        }

        # Abstract field.
    } elsif ($field eq 'Abstract') {
        # The three following variables are used for checking if the field is
        # correctly phrased.  We detect if each line (except for the first
        # line and lines containing single dot) of the field starts with the
        # same number of spaces, not followed by the same non-space character,
        # and the number of spaces is > 1.
        #
        # We try to match fields like this:
        #  ||Abstract: The Boost web site provides free peer-reviewed portable
        #  ||  C++ source libraries.  The emphasis is on libraries which work
        #  ||  well with the C++ Standard Library.  One goal is to establish
        #
        # but not like this:
        #  ||Abstract:  This is "Ding"
        #  ||  * a dictionary lookup program for Unix,
        #  ||  * DIctionary Nice Grep,
        my $leadsp;            # string with leading spaces from second line
        my $charafter;         # first non-whitespace char of second line
        my $leadsp_ok = 1;     # are spaces OK?

        # Intentionally skipping the first line.
        for my $idx (1 .. $#{$vals}) {
            $_ = $vals->[$idx];

            if (/manage\s+online\s+manuals\s.*Debian/) {
                $self->pointed_hint('doc-base-abstract-field-is-template',
                    $item->pointer($position))
                  unless $pkg eq 'doc-base';

            } elsif (/^(\s+)\.(\s*)$/ and ($1 ne $SPACE or $2)) {
                $self->pointed_hint(
                    'doc-base-abstract-field-separator-extra-whitespace',
                    $item->pointer($position - $#{$vals} + $idx)
                );

            } elsif (!$leadsp && /^(\s+)(\S)/) {
                # The regexp should always match.
                ($leadsp, $charafter) = ($1, $2);
                $leadsp_ok = $leadsp eq $SPACE;

            } elsif (!$leadsp_ok && /^(\s+)(\S)/) {
                # The regexp should always match.
                undef $charafter if $charafter && $charafter ne $2;
                $leadsp_ok = 1
                  if ($1 ne $leadsp) || ($1 eq $leadsp && $charafter);
            }
        }

        unless ($leadsp_ok) {
            $self->pointed_hint(
                'doc-base-abstract-might-contain-extra-leading-whitespace',
                $item->pointer($position));
        }

        # Check spelling.
        if (@{$vals}) {
            my $stag_emitter
              = $self->spelling_tag_emitter(
                'spelling-error-in-doc-base-abstract-field',
                $item->pointer($position));
            check_spelling(
                $self->data,
                join($SPACE, @{$vals}),
                $group->spelling_exceptions,
                $stag_emitter
            );
            check_spelling_picky($self->data, join($SPACE, @{$vals}),
                $stag_emitter);
        }
    }

    return;
}

# Checks the section of the doc-base control file.  Tries to find required
# fields missing in the section.
sub check_doc_base_file_section {
    my ($self, $item, $position, $sawfields, $sawformats, $knownfields) = @_;

    $self->pointed_hint('doc-base-file-no-format', $item->pointer($position))
      if ((defined $sawfields->{'Files'} || defined $sawfields->{'Index'})
        && !(defined $sawfields->{'Format'}));

    # The current format is set by check_doc_base_field.
    if ($sawfields->{'Format'}) {
        my $format =  $sawformats->{' *current* '};
        $self->pointed_hint('doc-base-file-no-index',$item->pointer($position))
          if ( $format
            && ($format eq 'html' || $format eq 'info')
            && !$sawfields->{'Index'});
    }
    for my $field (sort keys %{$knownfields}) {
        $self->pointed_hint('doc-base-file-lacks-required-field',
            $item->pointer($position), $field)
          if ($knownfields->{$field} == 1 && !$sawfields->{$field});
    }

    return;
}

# Add file and link to $all_files and $all_links.  Note that both files and
# links have to include a leading /.
sub add_file_link_info {
    my ($processable, $file, $all_files, $all_links) = @_;

    my $link = $processable->installed->lookup($file)->link;
    my $ishard = $processable->installed->lookup($file)->is_hardlink;

    # make name absolute
    $file = $SLASH . $file
      unless $file =~ m{^/};

    $file =~ s{/+}{/}g;                           # remove duplicated `/'
    $all_files->{$file} = 1;

    if (length $link) {

        $link = $DOT . $SLASH . $link
          if $link !~ m{^/};

        if ($ishard) {
            $link =~ s{^\./}{/};
        } elsif ($link !~ m{^/}) {            # not absolute link
            $link
              = $SLASH
              . $link;                  # make sure link starts with '/'
            $link =~ s{/+\./+}{/}g;                # remove all /./ parts
            my $dcount = 1;
            while ($link =~ s{^/+\.\./+}{/}) {     #\ count & remove
                $dcount++;                         #/ any leading /../ parts
            }
            my $f = $file;
            while ($dcount--) {                   #\ remove last $dcount
                $f=~ s{/[^/]*$}{};                #/ path components from $file
            }
            $link
              = $f. $link;                   # now we should have absolute link
        }
        $all_links->{$file} = $link unless ($link eq $file);
    }

    return;
}

# Dereference all symlinks in file.
sub delink {
    my ($file, $all_links) = @_;

    $file =~ s{/+}{/}g;                            # remove duplicated '/'
    return $file
      unless %{$all_links};              # package doesn't symlinks

    my $p1 = $EMPTY;
    my $p2 = $file;
    my %used_links;

    # In the loop below we split $file into two parts on each '/' until
    # there's no remaining slashes.  We try substituting the first part with
    # corresponding symlink and if it succeeds, we start the procedure from
    # beginning.
    #
    # Example:
    #    Let $all_links{"/a/b"} == "/d", and $file == "/a/b/c"
    #    Then 0) $p1 == "",     $p2 == "/a/b/c"
    #         1) $p1 == "/a",   $p2 == "/b/c"
    #         2) $p1 == "/a/b", $p2 == "/c"      ; substitute "/d" for "/a/b"
    #         3) $p1 == "",     $p2 == "/d/c"
    #         4) $p1 == "/d",   $p2 == "/c"
    #         5) $p1 == "/d/c", $p2 == ""
    #
    # Note that the algorithm supposes, that
    #    i) $all_links{$X} != $X for each $X
    #   ii) both keys and values of %all_links start with '/'

    while (($p2 =~ s{^(/[^/]*)}{}g) > 0) {
        $p1 .= $1;
        if (defined $all_links->{$p1}) {
            return '!!! SYMLINK LOOP !!!' if defined $used_links{$p1};
            $p2 = $all_links->{$p1} . $p2;
            $p1 = $EMPTY;
            $used_links{$p1} = 1;
        }
    }

    # After the loop $p2 should be empty and $p1 should contain the target
    # file.  In some rare cases when $file contains no slashes, $p1 will be
    # empty and $p2 will contain the result (which will be equal to $file).
    return $p1 ne $EMPTY ? $p1 : $p2;
}

sub check_script {
    my ($self, $item, $pres) = @_;

    my $pkg = $self->processable->name;

    my ($no_check_menu, $no_check_installdocs);

    # control files are regular files and not symlinks, pipes etc.
    return
      unless defined $item;

    return
      if $item->is_symlink;

    return
      unless $item->is_open_ok;

    # nothing to do for ELF
    return
      if $item->is_elf;

    my $interpreter = $item->interpreter || 'unknown';

    if ($item->is_shell_script) {
        $interpreter = 'sh';

    } elsif ($interpreter =~ m{^/usr/bin/perl}) {
        $interpreter = 'perl';
    }

    open(my $fd, '<', $item->unpacked_path)
      or die encode_utf8('Cannot open ' . $item->unpacked_path);

    my $position = 1;
    while (my $line = <$fd>) {
        # skip comments
        $line =~ s/\#.*$//;

        ##
        # update-menus will satisfy the checks that the menu file
        # installed is properly used
        ##

        # does the script check whether update-menus exists?
        $pres->{'checks-for-updatemenus'} = $item->pointer($position)
          if $line =~ /-x\s+\S*update-menus/
          || $line =~ /(?:which|type)\s+update-menus/
          || $line =~ /command\s+.*?update-menus/;

        # does the script call update-menus?
        # TODO this regex-magic should be moved to some lib for checking
        # whether a certain word is likely called as command... --Jeroen
        if (
            $line =~m{ (?:^\s*|[;&|]\s*|(?:then|do|exec)\s+)
               (?:\/usr\/bin\/)?update-menus
               (?:\s|[;&|<>]|\Z)}xsm
        ) {
            # yes, it does.
            $pres->{'calls-updatemenus'} = $item->pointer($position);

            # checked first?
            if (not $pres->{'checks-for-updatemenus'} and $pkg ne 'menu') {
                $self->pointed_hint(
'maintainer-script-does-not-check-for-existence-of-updatemenus',
                    $item->pointer($position)
                ) unless $no_check_menu++;
            }
        }

        # does the script check whether install-docs exists?
        $pres->{'checks-for-installdocs'} = $item->pointer($position)
          if $line =~ s/-x\s+\S*install-docs//
          || $line =~/(?:which|type)\s+install-docs/
          || $line =~ s/command\s+.*?install-docs//;

        # does the script call install-docs?
        if (
            $line =~ m{ (?:^\s*|[;&|]\s*|(?:then|do)\s+)
               (?:\/usr\/sbin\/)?install-docs
               (?:\s|[;&|<>]|\Z) }xsm
        ) {
            # yes, it does.  Does it remove or add a doc?
            if ($line =~ /install-docs\s+(?:-r|--remove)\s/) {
                $pres->{'calls-installdocs-r'} = $item->pointer($position);
            } else {
                $pres->{'calls-installdocs'} = $item->pointer($position);
            }

            # checked first?
            if (not $pres->{'checks-for-installdocs'}) {
                $self->pointed_hint(
'maintainer-script-does-not-check-for-existence-of-installdocs',
                    $item->pointer($position)
                ) unless $no_check_installdocs++;
            }
        }

    } continue {
        ++$position;
    }

    close($fd);

    return;
}

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et
