# cruft -- lintian check script -*- perl -*-
#
# based on debhelper check,
# Copyright (C) 1999 Joey Hess
# Copyright (C) 2000 Sean 'Shaleh' Perry
# Copyright (C) 2002 Josip Rodin
# Copyright (C) 2007 Russ Allbery
#
# 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 http://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::cruft;
use strict;

use Dep;
use Tags;
use Util;

use Cwd;
use File::Find;
use File::Basename;

# All the packages that may provide config.{sub,guess} during the build, used
# to suppress warnings about outdated autotools helper files.
my $autotools_pkgs = join ' | ',
    qw(autotools-dev automake automaken automake1.4 automake1.7 automake1.8
       automake1.9 automake1.10);

# Directory checks.  These regexes match a directory that shouldn't be in the
# source package and associate it with a tag (minus the leading
# source-contains or diff-contains).  Note that only one of these regexes
# should trigger for any single directory.
my @directory_checks =
    ([ qr,^(.+/)?CVS$,        => 'cvs-control-dir'  ],
     [ qr,^(.+/)?\.svn$,      => 'svn-control-dir'  ],
     [ qr,^(.+/)?\.bzr$,      => 'bzr-control-dir'  ],
     [ qr,^(.+/)?\{arch\}$,   => 'arch-control-dir' ],
     [ qr,^(.+/)?\.arch-ids$, => 'arch-control-dir' ],
     [ qr!^(.+/)?,,.+$!       => 'arch-control-dir' ],
     [ qr,^(.+/)?\.git$,      => 'git-control-dir'  ],
     [ qr,^(.+/)?\.hg$,       => 'hg-control-dir'   ],
     [ qr,^(.+/)?\.be$,       => 'bts-control-dir'  ],
     [ qr,^(.+/)?\.ditrack$,  => 'bts-control-dir'  ],
    );

# File checks.  These regexes match files that shouldn't be in the source
# package and associate them with a tag (minus the leading source-contains or
# diff-contains).  Note that only one of these regexes should trigger for any
# given file.  If the third column is a true value, don't issue this tag
# unless the file is included in the diff; it's too common in source packages
# and not important enough to worry about.
my @file_checks =
    ([ qr,^(.+/)?svn-commit\.(.+\.)?tmp$, => 'svn-commit-file'        ],
     [ qr,^(.+/)?svk-commit.+\.tmp$,      => 'svk-commit-file'        ],
     [ qr,^(.+/)?\.arch-inventory$,       => 'arch-inventory-file'    ],
     [ qr,^(.+/)?\.hgtags$,               => 'hg-tags-file'           ],
     [ qr,^(.+/)?\.\#(.+?)\.\d+(\.\d+)*$, => 'cvs-conflict-copy'      ],
     [ qr,^(.+/)?(.+?)\.(r\d+)$,          => 'svn-conflict-file'      ],
     [ qr,\.(orig|rej)$,                  => 'patch-failure-file',  1 ],
     [ qr,((^|/)\.[^/]+\.swp|~)$,         => 'editor-backup-file',  1 ],
    );

my $dir;
my $atdinbd;

sub run {

my $pkg = shift;
my $type = shift;
my $info = shift;

if (-e "debfiles/files" and not -z "debfiles/files") {
    tag 'debian-files-list-in-source';
}

# This doens't really belong here, but there isn't a better place at the
# moment to put this check.
if ($info->native) {
    my $version = $info->field('version');
    if ($version =~ /-/ and $version !~ /-0\.[^-]+$/) {
        tag 'native-package-with-dash-version';
    }
}

# Check if this is a documentation package that's not arch: all.  This doesn't
# really belong here either.
my $arch;
if (defined $info->field('architecture')) {
    my $arch = $info->field('architecture');
    if ($pkg =~ /-docs?$/ && $arch ne 'all') {
        tag 'documentation-package-not-architecture-independent';
    }
}

# Read build-depends file and see if it depends on autotools-dev or automake.
# I'm not thrilled with having the automake exception as well, but people do
# depend on autoconf and automake and then use autoreconf to update
# config.guess and config.sub, and automake depends on autotools-dev.
$atdinbd = 0;
if (defined $info->field('build-depends')) {
    my $bd = $info->field('build-depends');
    $atdinbd = 1 if Dep::implies(Dep::parse($bd), Dep::parse($autotools_pkgs));
}

# Create a closure so that we can pass our lexical variables into the find
# wanted function.  We don't want to make them global because we'll then leak
# that data across packages in a large Lintian run.
my %warned;
check_diffstat("diffstat", \%warned) unless $info->native;
my $wanted = sub { find_cruft($pkg, $info, \%warned, $atdinbd) };
find($wanted, 'unpacked');

} # </run>

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

# Check the diff for problems.  Record any files we warn about in $warned so
# that we don't warn again when checking the full unpacked source.  Takes the
# name of a file containing diffstat output.
sub check_diffstat {
    my ($diffstat, $warned) = @_;
    my $saw_file;
    open(STAT, '<', $diffstat) or fail("cannot open $diffstat: $!");
    local $_;
    while (<STAT>) {
        my ($file) = (m,^\s+(.*?)\s+\|,)
            or fail("syntax error in diffstat file: $_");
        $saw_file = 1;

        # Check for CMake cache files.  These embed the source path and hence
        # will cause FTBFS on buildds, so they should never be touched in the
        # diff.
        if ($file =~ m,(^|/)CMakeCache.txt\z,) {
            tag 'diff-contains-cmake-cache-file', $file;
        }

        # For everything else, we only care about diffs that add files.  If
        # the file is being modified, that's not a problem with the diff and
        # we'll catch it later when we check the source.  This regex doesn't
        # catch only file adds, just any diff that doesn't remove lines from a
        # file, but it's a good guess.
        next unless m,\|\s+\d+\s+\++$,;

        # diffstat output contains only files, but we consider the directory
        # checks to trigger if the diff adds any files in those directories.
        my ($directory) = ($file =~ m,^(.*)/[^/]+$,);
        if ($directory and not $warned->{$directory}) {
            for my $rule (@directory_checks) {
                if ($directory =~ /$rule->[0]/) {
                    tag "diff-contains-$rule->[1]", $directory;
                    $warned->{$directory} = 1;
                }
            }
        }

        # Now the simpler file checks.
        for my $rule (@file_checks) {
            if ($file =~ /$rule->[0]/) {
                tag "diff-contains-$rule->[1]", $file;
                $warned->{$file} = 1;
            }
        }

        # Additional special checks only for the diff, not the full source.
        if ($file =~ m,^debian/substvars$,) {
            tag 'diff-contains-substvars', $file;
        }
    }
    close(STAT) or fail("error reading diffstat file: $!");

    # If there was nothing in the diffstat output, there was nothing in the
    # diff, which is probably a mistake.
    tag 'empty-debian-diff' unless $saw_file;
}

# Check each file in the source package for problems.  By the time we get to
# this point, we've already checked the diff and warned about anything added
# there, so we only warn about things that weren't in the diff here.
#
# Report problems with native packages using the "diff-contains" rather than
# "source-contains" tag.  The tag isn't entirely accurate, but it's better
# than creating yet a third set of tags, and this gets the severity right.
#
# Exclude the lintian test suites from these checks.
sub find_cruft {
    my ($pkg, $info, $warned, $atdinbd) = @_;
    (my $name = $File::Find::name) =~ s,^(\./)?unpacked/,,;
    return if $name =~ m,^t(?:estset)?/, and $pkg eq 'lintian';

    my $prefix = ($info->native ? "diff-contains" : "source-contains");
    if (-d and not $warned->{$name}) {
        for my $rule (@directory_checks) {
            if ($name =~ /$rule->[0]/) {
                tag "${prefix}-$rule->[1]", $name;
            }
        }
    }
    -f or return; # we just need normal files for the rest

    unless ($warned->{$name}) {
        for my $rule (@file_checks) {
            next if ($rule->[2] and not $info->native);
            if ($name =~ /$rule->[0]/) {
                tag "${prefix}-$rule->[1]", $name;
            }
        }
    }

    # Tests of autotools files are a special case.  Ignore debian/config.cache
    # as anyone doing that probably knows what they're doing and is using it
    # as part of the build.
    if ($name =~ m,^(.+/)?config.(?:cache|log|status)$,) {
        if ($name !~ m,^debian/config\.cache$,) {
            tag "configure-generated-file-in-source", $name;
        }
    } elsif ($name =~ m,^(.+/)?config.(?:guess|sub)$, and not $atdinbd) {
        my $b = basename $name;
        open (F, '<', $b) or die "can't open $name: $!";
        while (<F>) {
            last if $. > 10; # it's on the 6th line, but be a bit more lenient
            if (/^(?:timestamp|version)='(\d+)(.+)'$/ and $1 < 2004) {
                tag "outdated-autotools-helper-file", $name, "$1$2";
            }
        }
        close F;
    } elsif ($name =~ m,^(.+/)?ltconfig$,) {
        tag "ancient-libtool", $name;
    } elsif ($name =~ m,^(.+/)?ltmain\.sh$,) {
        my $b = basename $name;
        open (F, '<', $b) or die "can't open $name: $!";
        while (<F>) {
            if (/^VERSION=[\"\']?(1\.(\d)\.(\d+)(?:-(\d))?)/) {
                my ($version, $major, $minor, $debian) = ($1, $2, $3, $4);
                if ($major < 5 or ($major == 5 and $minor < 2)) {
                    tag "ancient-libtool", $name, $version;
                } elsif ($minor == 2 and (!$debian or $debian < 2)) {
                    tag "ancient-libtool", $name, $version;
                } elsif ($minor < 24) {
                    # not entirely sure whether that would be good idea
#                    tag "outdated-libtool", $name, $version;
                }
                last;
            }
        }
        close F;
    }
}

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: ts=8 sw=4 noet syntax=perl
