# copyright -- lintian check script -*- perl -*-

# Copyright (C) 1998 Christian Schwarz
# Copyright (C) 1998 Richard Braakman
# Copyright (C) 2011 Jakub Wilk
# Copyright (C) 2020 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::Debian::Copyright;

use v5.20;
use warnings;
use utf8;

use Const::Fast;
use List::SomeUtils qw(any all none uniq);
use Path::Tiny;
use Syntax::Keyword::Try;
use Unicode::UTF8 qw(valid_utf8 decode_utf8 encode_utf8);

use Lintian::Deb822;
use Lintian::IPC::Run3 qw(safe_qx);
use Lintian::Spelling qw(check_spelling);

use Moo;
use namespace::clean;

with 'Lintian::Check';

const my $EMPTY => q{};

const my $APPROXIMATE_GPL_LENGTH => 12_000;
const my $APPROXIMATE_GFDL_LENGTH => 12_000;
const my $APPROXIMATE_APACHE_2_LENGTH => 10_000;

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

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

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

    my $debian_dir = $self->processable->patched->resolve_path('debian/');
    return
      unless defined $debian_dir;

    my @installables = $self->processable->debian_control->installables;
    my @additional = map { $_ . '.copyright' } @installables;

    my @candidates = ('copyright', @additional);
    my @files = grep { defined } map { $debian_dir->child($_) } @candidates;

    # look for <pkgname>.copyright for a single installable
    if (@files == 1) {
        my $single = $files[0];

        $self->pointed_hint('named-copyright-for-single-installable',
            $single->pointer)
          unless $single->name eq 'debian/copyright';
    }

    $self->hint('no-debian-copyright-in-source')
      unless @files;

    my @symlinks = grep { $_->is_symlink } @files;
    $self->pointed_hint('debian-copyright-is-symlink', $_->pointer)
      for @symlinks;

    return;
}

# no copyright in udebs
sub binary {
    my ($self) = @_;

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

    # looking up entry without slash first; index should not be so picky
    my $doclink
      = $self->processable->installed->lookup("usr/share/doc/$package");
    if ($doclink && $doclink->is_symlink) {

        # check if this symlink references a directory elsewhere
        if ($doclink->link =~ m{^(?:\.\.)?/}s) {
            $self->pointed_hint(
                'usr-share-doc-symlink-points-outside-of-usr-share-doc',
                $doclink->pointer, $doclink->link);
            return;
        }

        # The symlink may point to a subdirectory of another
        # /usr/share/doc directory.  This is allowed if this
        # package depends on link and both packages come from the
        # same source package.
        #
        # Policy requires that packages be built from the same
        # source if they're going to do this, which by my (rra's)
        # reading means that we should have a strict version
        # dependency.  However, in practice the copyright file
        # doesn't change a lot and strict version dependencies
        # cause other problems (such as with arch: any / arch: all
        # package combinations and binNMUs).
        #
        # We therefore just require the dependency for now and
        # don't worry about the version number.
        my $link = $doclink->link;
        $link =~ s{/.*}{};

        unless ($self->depends_on($self->processable, $link)) {
            $self->hint('usr-share-doc-symlink-without-dependency', $link);

            return;
        }

        # Check if the link points to a package from the same source.
        $self->check_cross_link($link);

        return;
    }

    # now with a slash; indicates directory
    my $docdir
      = $self->processable->installed->lookup("usr/share/doc/$package/");
    unless ($docdir) {
        $self->hint('no-copyright-file');
        return;
    }

    my $found = 0;
    my $zipped = $docdir->child('copyright.gz');
    if (defined $zipped) {

        $self->pointed_hint('copyright-file-compressed', $zipped->pointer);
        $found = 1;
    }

    my $linked = 0;

    my $item = $docdir->child('copyright');
    if (defined $item) {
        $found = 1;

        if ($item->is_symlink) {

            $self->pointed_hint('copyright-file-is-symlink', $item->pointer);
            $linked = 1;
         # fall through; coll/copyright-file prevents reading through evil link
        }
    }

    unless ($found) {

        # #522827: special exception for perl for now
        $self->hint('no-copyright-file')
          unless $package eq 'perl';

        return;
    }

    my $copyrigh_path;

    my $uncompressed
      = $self->processable->installed->resolve_path(
        "usr/share/doc/$package/copyright");
    $copyrigh_path = $uncompressed->unpacked_path
      if defined $uncompressed;

    my $compressed
      = $self->processable->installed->resolve_path(
        "usr/share/doc/$package/copyright.gz");
    if (defined $compressed) {

        my $bytes = safe_qx('gunzip', '-c', $compressed->unpacked_path);
        my $contents = decode_utf8($bytes);

        my $extracted
          = path($self->processable->basedir)->child('copyright')->stringify;
        path($extracted)->spew($contents);

        $copyrigh_path = $extracted;
    }

    return
      unless length $copyrigh_path;

    my $bytes = path($copyrigh_path)->slurp;

    # another check complains about invalid encoding
    return
      unless valid_utf8($bytes);

    # check contents of copyright file
    my $contents = decode_utf8($bytes);

    $self->hint('copyright-has-crs')
      if $contents =~ /\r/;

    my $wrong_directory_detected = 0;

    my $KNOWN_COMMON_LICENSES
      =  $self->data->load('copyright-file/common-licenses');

    if ($contents =~ m{ (usr/share/common-licenses/ ( [^ \t]*? ) \.gz) }xsm) {
        my ($path, $license) = ($1, $2);
        if ($KNOWN_COMMON_LICENSES->recognizes($license)) {
            $self->hint('copyright-refers-to-compressed-license', $path);
        }
    }

    # Avoid complaining about referring to a versionless license file
    # if the word "version" appears nowhere in the copyright file.
    # This won't catch all of our false positives for GPL references
    # that don't include a specific version number, but it will get
    # the obvious ones.
    if ($contents =~ m{(usr/share/common-licenses/(L?GPL|GFDL))([^-])}i) {
        my ($ref, $license, $separator) = ($1, $2, $3);
        if ($separator =~ /[\d\w]/) {
            $self->hint('copyright-refers-to-nonexistent-license-file',
                "$ref$separator");
        } elsif ($contents =~ /\b(?:any|or)\s+later(?:\s+version)?\b/i
            || $contents =~ /License: $license-[\d\.]+\+/i
            || $contents =~ /as Perl itself/i
            || $contents =~ /License-Alias:\s+Perl/
            || $contents =~ /License:\s+Perl/) {
            $self->hint('copyright-refers-to-symlink-license', $ref);
        } else {
            $self->hint('copyright-refers-to-versionless-license-file', $ref)
              if $contents =~ /\bversion\b/;
        }
    }

    # References to /usr/share/common-licenses/BSD are deprecated as of Policy
    # 3.8.5.
    if ($contents =~ m{/usr/share/common-licenses/BSD}) {
        $self->hint('copyright-refers-to-deprecated-bsd-license-file');
    }

    if ($contents =~ m{(usr/share/common-licences)}) {
        $self->hint('copyright-refers-to-incorrect-directory', $1);
        $wrong_directory_detected = 1;
    }

    if ($contents =~ m{usr/share/doc/copyright}) {
        $self->hint('copyright-refers-to-old-directory');
        $wrong_directory_detected = 1;
    }

    if ($contents =~ m{usr/doc/copyright}) {
        $self->hint('copyright-refers-to-old-directory');
        $wrong_directory_detected = 1;
    }

    # Lame check for old FSF zip code.  Try to avoid false positives from other
    # Cambridge, MA addresses.
    if ($contents
        =~ m/(?:Free\s*Software\s*Foundation.*02139|02111-1307|02110-1301)/s) {
        $self->hint('old-fsf-address-in-copyright-file');
    }

    # Whether the package is covered by the GPL, used later for the
    # libssl check.
    my $gpl;

    if (
        length $contents > $APPROXIMATE_GPL_LENGTH
        && (
            $contents =~ m{  \b \QGNU GENERAL PUBLIC LICENSE\E \s*
                    \QTERMS AND CONDITIONS FOR COPYING,\E \s*
                    \QDISTRIBUTION AND MODIFICATION\E \b }msx
            || (
                $contents =~ m{ \b \QGNU GENERAL PUBLIC LICENSE\E
                                   \s* \QVersion 3\E }msx
                && $contents =~ m{ \b \QTERMS AND CONDITIONS\E \s }msx
            )
        )
    ) {
        $self->hint('copyright-file-contains-full-gpl-license');
        $gpl = 1;
    }

    if (
        length $contents > $APPROXIMATE_GFDL_LENGTH
        && $contents =~ m{ \b \QGNU Free Documentation License\E
                           \s* \QVersion 1.2\E }msx
        && $contents =~ m{ \b \Q1. APPLICABILITY AND DEFINITIONS\E }msx
    ) {

        $self->hint('copyright-file-contains-full-gfdl-license');
    }

    if (   length $contents > $APPROXIMATE_APACHE_2_LENGTH
        && $contents =~ m{ \b \QApache License\E \s+ \QVersion 2.0,\E }msx
        && $contents
        =~ m{ \QTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\E }msx
    ) {

        $self->hint('copyright-file-contains-full-apache-2-license');
    }

    # wtf?
    if (   ($contents =~ m{common-licenses(/\S+)})
        && ($contents !~ m{/usr/share/common-licenses/})) {
        $self->hint('copyright-does-not-refer-to-common-license-file', $1);
    }

    # This check is a bit prone to false positives, since some other
    # licenses mention the GPL.  Also exclude any mention of the GPL
    # following what looks like mail header fields, since sometimes
    # e-mail discussions of licensing are included in the copyright
    # file but aren't referring to the license of the package.
    unless (
        $contents =~ m{/usr/share/common-licenses}
        || $contents =~ m/Zope Public License/
        || $contents =~ m/LICENSE AGREEMENT FOR PYTHON 1.6.1/
        || $contents =~ m/LaTeX Project Public License/
        || $contents
        =~ m/(?:^From:.*^To:|^To:.*^From:).*(?:GNU General Public License|GPL)/ms
        || $contents =~ m/AFFERO GENERAL PUBLIC LICENSE/
        || $contents =~ m/GNU Free Documentation License[,\s]*Version 1\.1/
        || $contents =~ m/CeCILL FREE SOFTWARE LICENSE AGREEMENT/ #v2.0
        || $contents =~ m/FREE SOFTWARE LICENSING AGREEMENT CeCILL/ #v1.1
        || $contents =~ m/CNRI OPEN SOURCE GPL-COMPATIBLE LICENSE AGREEMENT/
        || $contents =~ m/compatible\s+with\s+(?:the\s+)?(?:GNU\s+)?GPL/
        || $contents =~ m/(?:GNU\s+)?GPL\W+compatible/
        || $contents
        =~ m/was\s+previously\s+(?:distributed\s+)?under\s+the\s+GNU/
        || $contents
        =~ m/means\s+either\s+the\s+GNU\s+General\s+Public\s+License/
        || $wrong_directory_detected
    ) {
        if (
            check_names_texts(
                $contents,
                qr/\b(?:GFDL|gnu[-_]free[-_]documentation[-_]license)\b/i,
                qr/GNU Free Documentation License|(?-i:\bGFDL\b)/i
            )
        ) {
            $self->hint('copyright-not-using-common-license-for-gfdl');
        }elsif (
            check_names_texts(
                $contents,
qr/\b(?:LGPL|gnu[-_](?:lesser|library)[-_]general[-_]public[-_]license)\b/i,
qr/GNU (?:Lesser|Library) General Public License|(?-i:\bLGPL\b)/i
            )
        ) {
            $self->hint('copyright-not-using-common-license-for-lgpl');
        }elsif (
            check_names_texts(
                $contents,
                qr/\b(?:GPL|gnu[-_]general[-_]public[-_]license)\b/i,
                qr/GNU General Public License|(?-i:\bGPL\b)/i
            )
        ) {
            $self->hint('copyright-not-using-common-license-for-gpl');
            $gpl = 1;
        }elsif (
            check_names_texts(
                $contents,qr/\bapache[-_]2/i,
                qr/\bApache License\s*,?\s*Version 2|\b(?-i:Apache)-2/i
            )
        ) {
            $self->hint('copyright-not-using-common-license-for-apache2');
        }
    }

    if (
        check_names_texts(
            $contents,
            qr/\b(?:perl|artistic)\b/,
            sub {
                my ($text) = @_;
                $text
                  =~ /(?:under )?(?:the )?(?:same )?(?:terms )?as Perl itself\b/i
                  && $text !~ m{usr/share/common-licenses/};
            }
        )
    ) {
        $self->hint('copyright-file-lacks-pointer-to-perl-license');
    }

    # Checks for various packaging helper boilerplate.

    $self->hint('helper-templates-in-copyright')
      if $contents =~ m{<fill in (?:http/)?ftp site>}
      || $contents =~ /<Must follow here>/
      || $contents =~ /<Put the license of the package here/
      || $contents =~ /<put author[\'\(]s\)? name and email here>/
      || $contents =~ /<Copyright \(C\) YYYY Name OfAuthor>/
      || $contents =~ /Upstream Author\(s\)/
      || $contents =~ /<years>/
      || $contents =~ /<special license>/
      || $contents
      =~ /<Put the license of the package here indented by 1 space>/
      || $contents
      =~ /<This follows the format of Description: lines\s*in control file>/
      || $contents =~ /<Including paragraphs>/
      || $contents =~ /<likewise for another author>/;

    # dh-make-perl
    $self->hint('copyright-contains-automatically-extracted-boilerplate')
      if $contents =~ /This copyright info was automatically extracted/;

    $self->hint('helper-templates-in-copyright')
      if $contents =~ /<INSERT COPYRIGHT YEAR\(S\) HERE>/;

    $self->hint('copyright-has-url-from-dh_make-boilerplate')
      if $contents =~ m{url://};

    # dh-make boilerplate
    my @dh_make_boilerplate = (
"# Please also look if there are files or directories which have a\n# different copyright/license attached and list them here.",
"# If you want to use GPL v2 or later for the /debian/* files use\n# the following clauses, or change it to suit. Delete these two lines"
    );

    $self->hint('copyright-contains-dh_make-todo-boilerplate')
      if any { $contents =~ /$_/ } @dh_make_boilerplate;

    $self->hint('copyright-with-old-dh-make-debian-copyright')
      if $contents =~ /The\s+Debian\s+packaging\s+is\s+\(C\)\s+\d+/i;

    # Other flaws in the copyright phrasing or contents.
    if ($found && !$linked) {
        $self->hint('copyright-without-copyright-notice')
          unless $contents
          =~ m{(?:Copyright|Copr\.|\N{COPYRIGHT SIGN})(?:.*|[\(C\):\s]+)\b\d{4}\b
               |\bpublic(?:\s+|-)domain\b}xi;
    }

    check_spelling(
        $self->data,$contents,
        $self->group->spelling_exceptions,
        $self->spelling_tag_emitter('spelling-error-in-copyright'), 0
    );

    # Now, check for linking against libssl if the package is covered
    # by the GPL.  (This check was requested by ftp-master.)  First,
    # see if the package is under the GPL alone and try to exclude
    # packages with a mix of GPL and LGPL or Artistic licensing or
    # with an exception or exemption.
    if (($gpl || $contents =~ m{/usr/share/common-licenses/GPL})
        &&$contents
        !~ m{exception|exemption|/usr/share/common-licenses/(?!GPL)\S}){

        my @depends
          = split(/\s*,\s*/,$self->processable->fields->value('Depends'));
        my @predepends
          = split(/\s*,\s*/,$self->processable->fields->value('Pre-Depends'));

        $self->hint('possible-gpl-code-linked-with-openssl')
          if any { /^libssl[0-9.]+(?:\s|\z)/ && !/\|/ }(@depends, @predepends);
    }

    return;
} # </run>

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

# Returns true if the package whose information is in $processable depends $package
# or if $package is essential.
sub depends_on {
    my ($self, $processable, $package) = @_;

    my $KNOWN_ESSENTIAL = $self->data->load('fields/essential');

    return 1
      if $KNOWN_ESSENTIAL->recognizes($package);

    my $strong = $processable->relation('strong');
    return 1
      if $strong->satisfies($package);

    my $arch = $processable->architecture;
    return 1
      if $arch ne 'all' and $strong->satisfies("${package}:${arch}");

    return 0;
}

# Checks cross pkg links for /usr/share/doc/$pkg links
sub check_cross_link {
    my ($self, $foreign) = @_;

    my $source = $self->group->source;
    if ($source) {

        # source package is available; check its list of binaries
        return
          if any { $foreign eq $_ } $source->debian_control->installables;

        $self->hint('usr-share-doc-symlink-to-foreign-package', $foreign);

    } else {
        # The source package is not available, but the binary could
        # be present anyway;  If they are in the same group, they claim
        # to have the same source (and source version)
        return
          if any { $_->name eq $foreign }$self->group->get_installables;

        # It was not, but since the source package was not present, we cannot
        # tell if it is foreign or not at this point.

        $self->hint(
'cannot-check-whether-usr-share-doc-symlink-points-to-foreign-package'
        );
    }

    return;
}

# Checks the name and text of every license in the file against given name and
# text check coderefs, if the file is in the new format, if the file is in the
# old format only runs the text coderef against the whole file.
sub check_names_texts {
    my ($contents, $name_check, $action) = @_;

    my $text_check;

    if ((ref($action) || $EMPTY) eq 'Regexp') {
        $text_check = sub {
            my ($textref) = @_;
            return ${$textref} =~ $action;
        };

    } else {
        $text_check = sub {
            my ($textref) = @_;
            return $action->(${$textref});
        };
    }

    my $deb822 = Lintian::Deb822->new;

    my @paragraphs;
    try {
        @paragraphs = $deb822->parse_string($contents);

    } catch {
        # parse error: copyright not in new format, just check text
        return $text_check->(\$contents);
    }

    my @licenses = grep { length } map { $_->value('License') } @paragraphs;
    for my $license (@licenses) {

        my ($name, $text) = ($license =~ /^\s*([^\r\n]+)\r?\n(.*)\z/s);

        next
          unless length $text;

        next
          if $text =~ /^[\s\r\n]*\z/;

        return 1
          if $name =~ $name_check
          && $text_check->(\$text);
    }

    # did not match anything
    return 0;
}

1;

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