# debian/changelog -- lintian check script -*- perl -*- # Copyright (C) 1998 Christian Schwarz # Copyright (C) 2019-2020 Chris Lamb # 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::Debian::Changelog; use v5.20; use warnings; use utf8; use Const::Fast; use Data::Validate::Domain; use Date::Format qw(time2str); use Email::Address::XS; use List::Util qw(first); use List::SomeUtils qw(any all uniq); use Path::Tiny; use Syntax::Keyword::Try; use Unicode::UTF8 qw(valid_utf8 decode_utf8 encode_utf8); use Lintian::Changelog; use Lintian::Changelog::Version; use Lintian::IPC::Run3 qw(safe_qx); use Lintian::Relation::Version qw(versions_gt); use Lintian::Spelling qw(check_spelling); const my $EMPTY => q{}; const my $DOUBLE_QUOTE => q{"}; const my $GREATER_THAN => q{>}; const my $APPROXIMATELY_EQUAL => q{~}; const my $NOT_EQUALS => q{!=}; const my $ARROW => q{->}; const my $MAXIMUM_WIDTH => 82; const my $FIRST_ARCHIVED_BUG_NUMBER => 50_004; const my $OUT_OF_REACH_BUG_NUMBER => 1_500_000; use Moo; use namespace::clean; with 'Lintian::Check'; sub spelling_tag_emitter { my ($self, $tag_name, $item, @orig_args) = @_; my $pointer = $item->pointer($.); return sub { return $self->pointed_hint($tag_name, $pointer, @orig_args, @_); }; } sub source { my ($self) = @_; my $pkg = $self->processable->name; my $processable = $self->processable; my $group = $self->group; my $changelog = $processable->changelog; return unless defined $changelog; my @entries = @{$changelog->entries}; return unless @entries; my $latest_entry = $entries[0]; my $changelog_item = $self->processable->changelog_item; my $latest_pointer = $changelog_item->pointer($latest_entry->position); my $changes = $group->changes; if ($changes) { my $contents = path($changes->path)->slurp; # make sure dot matches newlines, as well if ($contents =~ qr/BEGIN PGP SIGNATURE.*END PGP SIGNATURE/ms) { $self->pointed_hint('unreleased-changelog-distribution', $latest_pointer) if $latest_entry->Distribution eq 'UNRELEASED'; } } my $versionstring = $processable->fields->value('Version'); my $latest_version = Lintian::Changelog::Version->new; try { $latest_version->assign($versionstring, $processable->native); } catch { my $indicator= ($processable->native ? $EMPTY : 'non-') . 'native'; $self->pointed_hint('malformed-debian-changelog-version', $latest_pointer,$versionstring, "(for $indicator)"); undef $latest_version; # perlcritic 1.140-1 requires a semicolon on the next line }; if (defined $latest_version) { $self->pointed_hint( 'hyphen-in-upstream-part-of-debian-changelog-version', $latest_pointer,$latest_version->upstream) if !$processable->native && $latest_version->upstream =~ qr/-/; # unstable, testing, and stable shouldn't be used in Debian # version numbers. unstable should get a normal version # increment and testing and stable should get suite-specific # versions. # # NMUs get a free pass because they need to work with the # version number that was already there. unless (length $latest_version->source_nmu) { my $revision = $latest_version->maintainer_revision; my $distribution = $latest_entry->Distribution; $self->pointed_hint('version-refers-to-distribution', $latest_pointer,$latest_version->literal) if ($revision =~ /testing|(?:un)?stable/i) || ( ($distribution eq 'unstable'|| $distribution eq 'experimental') && $revision =~ /woody|sarge|etch|lenny|squeeze|stretch|buster/); } my $examine = $latest_version->maintainer_revision; $examine = $latest_version->upstream unless $processable->native; my $candidate_pattern = qr/rc|alpha|beta|pre(?:view|release)?/; my $increment_pattern = qr/[^a-z].*|\Z/; my ($candidate_string, $increment_string) = ($examine =~ m/[^~a-z]($candidate_pattern)($increment_pattern)/sm); if (length $candidate_string && !length $latest_version->source_nmu) { $increment_string //= $EMPTY; # remove rc-part and any preceding symbol my $expected = $examine; $expected =~ s/[\.\+\-\:]?\Q$candidate_string\E.*//; my $suggestion = "$expected~$candidate_string$increment_string"; $self->pointed_hint( 'rc-version-greater-than-expected-version', $latest_pointer, $examine, $GREATER_THAN, $expected, "(consider using $suggestion)", ) if $latest_version->maintainer_revision eq '1' || $latest_version->maintainer_revision=~ /^0(?:\.1|ubuntu1)?$/ || $processable->native; } } if (@entries > 1) { my $previous_entry = $entries[1]; my $latest_timestamp = $latest_entry->Timestamp; my $previous_timestamp = $previous_entry->Timestamp; my $previous_version = Lintian::Changelog::Version->new; try { $previous_version->assign($previous_entry->Version, $processable->native); } catch { my $indicator= ($processable->native ? $EMPTY : 'non-') . 'native'; $self->pointed_hint( 'odd-historical-debian-changelog-version', $changelog_item->pointer($previous_entry->position), $previous_entry->Version, "(for $indicator)" ); undef $previous_version; } if ($latest_timestamp && $previous_timestamp) { $self->pointed_hint( 'latest-debian-changelog-entry-without-new-date', $latest_pointer) if $latest_timestamp <= $previous_timestamp && lc($latest_entry->Distribution) ne 'unreleased'; } if (defined $latest_version) { # skip first for my $entry (@entries[1..$#entries]) { # cannot use parser; nativeness may differ my ($no_epoch) = ($entry->Version =~ qr/^(?:[^:]+:)?([^:]+)$/); next unless defined $no_epoch; # disallowed even if epochs differ; see tag description if ( $latest_version->no_epoch eq $no_epoch && $latest_entry->Source eq $entry->Source) { $self->pointed_hint( 'latest-debian-changelog-entry-reuses-existing-version', $latest_pointer, $latest_version->literal, $APPROXIMATELY_EQUAL, $entry->Version, '(last used: '. $entry->Date . ')' ); last; } } } if (defined $latest_version && defined $previous_version) { # a reused version literal is caught by the broader previous check # start with a reasonable default my $expected_previous = $previous_version->literal; $expected_previous = $latest_version->without_backport if $latest_version->backport_release && $latest_version->backport_revision && $latest_version->debian_without_backport ne '0'; # find an appropriate prior version for a source NMU if (length $latest_version->source_nmu) { # can only do first nmu for now $expected_previous = $latest_version->without_source_nmu if $latest_version->source_nmu eq '1' &&$latest_version->maintainer_revision =~ qr/\d+/ && $latest_version->maintainer_revision ne '0'; } $self->pointed_hint( 'changelog-file-missing-explicit-entry',$latest_pointer, $previous_version->literal, $ARROW, "$expected_previous (missing)", $ARROW, $latest_version->literal ) unless $previous_version->literal eq $expected_previous || $latest_entry->Distribution eq 'bullseye' || $previous_entry->Distribution eq 'bullseye' || $latest_entry->Distribution =~ /-security$/i; if ( $latest_version->epoch eq $previous_version->epoch && $latest_version->upstream eq$previous_version->upstream && $latest_entry->Source eq $previous_entry->Source && !$processable->native) { $self->pointed_hint( 'possible-new-upstream-release-without-new-version', $latest_pointer) if $latest_entry->Changes =~ /^\s*\*\s+new\s+upstream\s+(?:\S+\s+)?release\b/im; my $non_consecutive = 0; $non_consecutive = 1 if !length $latest_version->source_nmu && $latest_version->maintainer_revision =~ /^\d+$/ && $previous_version->maintainer_revision =~ /^\d+$/ && $latest_version->maintainer_revision != $previous_version->maintainer_revision + 1; $non_consecutive = 1 if $latest_version->maintainer_revision eq $previous_version->maintainer_revision && $latest_version->source_nmu =~ /^\d+$/ && $previous_version->source_nmu =~ /^\d+$/ && $latest_version->source_nmu != $previous_version->source_nmu + 1; $non_consecutive = 1 if $latest_version->source_nmu =~ /^\d+$/ && !length $previous_version->source_nmu && $latest_version->source_nmu != 1; $self->pointed_hint( 'non-consecutive-debian-revision', $latest_pointer,$previous_version->literal, $ARROW,$latest_version->literal )if $non_consecutive; } if ($latest_version->epoch ne $previous_version->epoch) { $self->pointed_hint( 'epoch-change-without-comment',$latest_pointer, $previous_version->literal, $ARROW, $latest_version->literal )unless $latest_entry->Changes =~ /\bepoch\b/im; $self->pointed_hint( 'epoch-changed-but-upstream-version-did-not-go-backwards', $latest_pointer,$previous_version->literal, $ARROW,$latest_version->literal ) unless $processable->native || versions_gt($previous_version->upstream, $latest_version->upstream); } } } return; } # no copyright in udebs sub binary { my ($self) = @_; my $pkg = $self->processable->name; my $processable = $self->processable; my $group = $self->group; my $is_symlink = 0; my $native_pkg; my $foreign_pkg; my @doc_files; # skip packages which have a /usr/share/doc/$pkg -> foo symlink my $docfile = $processable->installed->lookup("usr/share/doc/$pkg"); return if defined $docfile && $docfile->is_symlink; # trailing slash in indicates a directory my $docdir = $processable->installed->lookup("usr/share/doc/$pkg/"); @doc_files = grep { $_->is_file || $_->is_symlink } $docdir->children if defined $docdir; my @news_files = grep { $_->basename =~ m{\A NEWS\.Debian (?:\.gz)? \Z}ixsm }@doc_files; $self->pointed_hint('debian-news-file-not-compressed', $_->pointer) for grep { $_->basename !~ m{\.gz$} } @news_files; $self->pointed_hint('wrong-name-for-debian-news-file', $_->pointer) for grep { $_->basename =~ m{\.gz$} && $_->basename ne 'NEWS.Debian.gz' } @news_files; my @changelog_files = grep { $_->basename =~ m{\A changelog (?:\.html|\.Debian)? (?:\.gz)? \Z}xsm } @doc_files; # ubuntu permits symlinks; their profile suppresses the tag $self->pointed_hint('debian-changelog-file-is-a-symlink', $_->pointer) for grep { $_->is_symlink } @changelog_files; $self->pointed_hint('changelog-file-not-compressed', $_->pointer) for grep { $_->basename !~ m{ \.gz \Z}xsm } @changelog_files; # Check if changelog files are compressed with gzip -9. # It's a bit of an open question here what we should do # with a file named ChangeLog. If there's also a # changelog file, it might be a duplicate, or the packager # may have installed NEWS as changelog intentionally. for my $item (@changelog_files) { next unless $item->basename =~ m{ \.gz \Z}xsm; my $resolved = $item->resolve_path; next unless defined $resolved; $self->pointed_hint('changelog-not-compressed-with-max-compression', $item->pointer) unless $resolved->file_type =~ /max compression/; } my @html_changelogs = grep { $_->basename =~ /^changelog\.html(?:\.gz)?$/ } @changelog_files; my @text_changelogs = grep { $_->basename =~ /^changelog(?:\.gz)?$/ } @changelog_files; if (!@text_changelogs) { $self->pointed_hint('html-changelog-without-text-version', $_->pointer) for @html_changelogs; } my $packagepath = 'usr/share/doc/' . $self->processable->name; my $news_item = $self->processable->installed->resolve_path( "$packagepath/NEWS.Debian.gz"); my $news; if (defined $news_item && $news_item->is_file) { my $bytes = safe_qx('gunzip', '-c', $news_item->unpacked_path); # another check complains about invalid encoding if (valid_utf8($bytes)) { my $contents = decode_utf8($bytes); my $newslog = Lintian::Changelog->new; $newslog->parse($contents); for my $error (@{$newslog->errors}) { my $position = $error->[0]; my $condition = $error->[1]; my $pointer = $news_item->pointer($position); $self->pointed_hint('syntax-error-in-debian-news-file', $pointer, $DOUBLE_QUOTE . $condition . $DOUBLE_QUOTE); } # Some checks on the most recent entry. if ($newslog->entries && defined @{$newslog->entries}[0]) { $news = @{$newslog->entries}[0]; my $pointer = $news_item->pointer($news->position); $self->pointed_hint( 'debian-news-entry-has-strange-distribution', $pointer,$news->Distribution) if length $news->Distribution && $news->Distribution eq 'UNRELEASED'; check_spelling( $self->data, $news->Changes, $group->spelling_exceptions, $self->spelling_tag_emitter( 'spelling-error-in-news-debian', $news_item ) ); $self->pointed_hint('debian-news-entry-uses-asterisk',$pointer) if $news->Changes =~ /^ \s* [*] \s /x; } } } # is this a native Debian package? # If the version is missing, we assume it to be non-native # as it is the most likely case. my $source = $processable->fields->value('Source'); my $source_version; if ($processable->fields->declares('Source') && $source =~ m/\((.*)\)/) { $source_version = $1; } else { $source_version = $processable->fields->value('Version'); } if (defined $source_version) { $native_pkg = ($source_version !~ m/-/); } else { # We do not know, but assume it to non-native as it is # the most likely case. $native_pkg = 0; } $source_version = $processable->fields->value('Version') || '0-1'; $foreign_pkg = (!$native_pkg && $source_version !~ m/-0\./); # A version of 1.2.3-0.1 could be either, so in that # case, both vars are false if ($native_pkg) { # native Debian package if (any { m/^changelog(?:\.gz)?$/} map { $_->basename } @doc_files) { # everything is fine } elsif (my $chg = first {$_->basename =~ m/^changelog[.]debian(?:\.gz)$/i;} @doc_files) { $self->pointed_hint('wrong-name-for-changelog-of-native-package', $chg->pointer); } else { $self->hint( 'no-changelog', "usr/share/doc/$pkg/changelog.gz", '(native package)' ); } } else { # non-native (foreign :) Debian package # 1. check for upstream changelog my $found_upstream_text_changelog = 0; if ( any { m/^changelog(\.html)?(?:\.gz)?$/ } map { $_->basename } @doc_files ) { $found_upstream_text_changelog = 1 unless $1; # everything is fine } else { # search for changelogs with wrong file name for my $item (@doc_files) { if ( $item->basename =~ m/^change/i && $item->basename !~ m/debian/i) { $self->pointed_hint('wrong-name-for-upstream-changelog', $item->pointer); last; } } } # 2. check for Debian changelog if ( any { m/^changelog\.Debian(?:\.gz)?$/ } map { $_->basename } @doc_files ) { # everything is fine } elsif (my $chg = first {$_->basename =~ m/^changelog\.debian(?:\.gz)?$/i;} @doc_files) { $self->pointed_hint('wrong-name-for-debian-changelog-file', $chg->pointer); } else { if ($foreign_pkg && $found_upstream_text_changelog) { $self->hint('debian-changelog-file-missing-or-wrong-name'); } elsif ($foreign_pkg) { $self->hint( 'no-changelog', "usr/share/doc/$pkg/changelog.Debian.gz", '(non-native package)' ); } # TODO: if uncertain whether foreign or native, either # changelog.gz or changelog.debian.gz should exists # though... but no tests catches this (extremely rare) # border case... Keep in mind this is only happening if we # have a -0.x version number... So not my priority to fix # --Jeroen } } my $changelog_item = $self->processable->changelog_item; return unless defined $changelog_item; # another check complains about invalid encoding my $changelog = $processable->changelog; for my $error (@{$changelog->errors}) { my $position = $error->[0]; my $condition = $error->[1]; my $pointer = $changelog_item->pointer($position); $self->pointed_hint('syntax-error-in-debian-changelog', $pointer, $DOUBLE_QUOTE . $condition . $DOUBLE_QUOTE); } # Check for some things in the raw changelog file and compute the # "offset" to the first line of the first entry. We use this to # report the line number of "too-long" lines. (#657402) my $real_start = $self->check_dch($changelog_item); my @entries = @{$changelog->entries}; # all versions from the changelog my %allversions = map { $_ => 1 } grep { defined } map { $_->Version } @entries; # checks applying to all entries for my $entry (@entries) { my $position = $entry->position; my $version = $entry->Version; my $pointer = $changelog_item->pointer($position); if (length $entry->Maintainer) { my ($parsed) = Email::Address::XS->parse($entry->Maintainer); unless ($parsed->is_valid) { $self->pointed_hint( 'bogus-mail-host-in-debian-changelog', $pointer,$entry->Maintainer,"(for version $version)", ); next; } unless (all { length } ($parsed->address, $parsed->user, $parsed->host)) { $self->pointed_hint( 'bogus-mail-host-in-debian-changelog', $pointer,$parsed->format,"(for version $version)", ); next; } $self->pointed_hint( 'bogus-mail-host-in-debian-changelog', $pointer, $parsed->address,"(for version $version)", ) unless is_domain($parsed->host, {domain_disable_tld_validation => 1}); } } my $INVALID_DATES = $self->data->load('changelog-file/invalid-dates',qr/\s*=\>\s*/); if (@entries) { # checks related to the latest entry my $latest_entry = $entries[0]; my $latest_pointer = $changelog_item->pointer($latest_entry->position); my $latest_timestamp = $latest_entry->Timestamp; if ($latest_timestamp) { my $warned = 0; my $longdate = $latest_entry->Date; for my $re ($INVALID_DATES->all()) { if ($longdate =~ m/($re)/i) { my $match = $1; my $repl = $INVALID_DATES->value($re); $self->pointed_hint('invalid-date-in-debian-changelog', $latest_pointer,"($match", $ARROW, "$repl)"); $warned = 1; } } my ($weekday_declared, $numberportion) = split(m/,\s*/, $longdate, 2); $numberportion //= $EMPTY; my ($tz, $weekday_actual); if ($numberportion =~ m/[ ]+ ([^ ]+)\Z/xsm) { $tz = $1; $weekday_actual = time2str('%a', $latest_timestamp, $tz); } if (not $warned and $tz and $weekday_declared ne $weekday_actual) { my $real_weekday = time2str('%A', $latest_timestamp, $tz); my $short_date = time2str('%Y-%m-%d', $latest_timestamp, $tz); $self->pointed_hint('debian-changelog-has-wrong-day-of-week', $latest_pointer,"$short_date was a $real_weekday"); } } # there is more than one changelog entry if (@entries > 1) { my $previous_entry = $entries[1]; my $previous_timestamp = $previous_entry->Timestamp; $self->pointed_hint('latest-changelog-entry-without-new-date', $latest_pointer) if defined $latest_timestamp && defined $previous_timestamp && $latest_timestamp <= $previous_timestamp && $latest_entry->Distribution ne 'UNRELEASED'; my $latest_dist = lc $latest_entry->Distribution; my $previous_dist = lc $previous_entry->Distribution; $self->pointed_hint('experimental-to-unstable-without-comment', $latest_pointer) if $latest_dist eq 'unstable' && $previous_dist eq 'experimental' && $latest_entry->Changes !~ m{ \b to \s+ ['"\N{LEFT SINGLE QUOTATION MARK}\N{LEFT DOUBLE QUOTATION MARK}]? (?:unstable|sid) ['"\N{RIGHT SINGLE QUOTATION MARK}\N{RIGHT DOUBLE QUOTATION MARK}]? \b }imx; my $changes = $group->changes; if ($changes) { my $changes_dist= lc $changes->fields->value('Distribution'); my %codename; $codename{'unstable'} = 'sid'; my @normalized = uniq map { $codename{$_} // $_ } ($latest_dist, $changes_dist); $self->pointed_hint( 'changelog-distribution-does-not-match-changes-file', $latest_pointer,$latest_dist, $NOT_EQUALS, $changes_dist )unless @normalized == 1; } } # Some checks should only be done against the most recent # changelog entry. my $changes = $latest_entry->Changes || $EMPTY; if (@entries == 1) { if ($latest_entry->Version && $latest_entry->Version =~ /-1$/) { $self->pointed_hint('initial-upload-closes-no-bugs', $latest_pointer) unless @{ $latest_entry->Closes }; $self->pointed_hint( 'new-package-uses-date-based-version-number', $latest_pointer, $latest_entry->Version, '(better: 0~' . $latest_entry->Version .')' )if $latest_entry->Version =~ m/^\d{8}/; } $self->pointed_hint('changelog-is-dh_make-template', $latest_pointer) if $changes =~ /(?:#?\s*)(?:\d|n)+ is the bug number of your ITP/i; } while ($changes =~ /(closes[\s;]*(?:bug)?\#?\s?\d{6,})[^\w]/ig) { my $closes = $1; $self->pointed_hint('possible-missing-colon-in-closes', $latest_pointer, $closes) if length $closes; } if ($changes =~ m/(TEMP-\d{7}-[0-9a-fA-F]{6})/) { my $temporary_cve = $1; $self->pointed_hint( 'changelog-references-temp-security-identifier', $latest_pointer, $temporary_cve); } # check for bad intended distribution if ( $changes =~ m{uploads? \s+ to \s+ (?'intended'testing|unstable|experimental|sid)}xi ){ my $intended = lc($+{intended}); $intended = 'unstable' if $intended eq 'sid'; my $uploaded = $latest_entry->Distribution; $self->pointed_hint('bad-intended-distribution', $latest_pointer, "intended for $intended but uploaded to $uploaded") if $uploaded ne $intended && $uploaded ne 'UNRELEASED'; } if ($changes =~ m{ (Close: \s+ [#] \d+) }xi) { my $statement = $1; $self->pointed_hint('misspelled-closes-bug', $latest_pointer, $statement); } my $changesempty = $changes; $changesempty =~ s/\W//gms; $self->pointed_hint('changelog-empty-entry', $latest_pointer) if !length $changesempty && $latest_entry->Distribution ne 'UNRELEASED'; # before bug 50004 bts removed bug instead of archiving for my $bug (@{$latest_entry->Closes}) { $self->pointed_hint('improbable-bug-number-in-closes', $latest_pointer, $bug) if $bug < $FIRST_ARCHIVED_BUG_NUMBER || $bug >= $OUT_OF_REACH_BUG_NUMBER; } # Compare against NEWS.Debian if available. for my $field (qw/Distribution Urgency/) { $self->pointed_hint( 'changelog-news-debian-mismatch', $news_item->pointer($news->position), $field, $latest_entry->$field, $NOT_EQUALS, $news->$field ) if defined $news && length $news->Version && $news->Version eq $latest_entry->Version && $news->$field ne $latest_entry->$field; } $self->pointed_hint( 'debian-news-entry-has-unknown-version', $news_item->pointer($news->position), $news->Version ) if defined $news && length $news->Version && !exists $allversions{$news->Version}; # Parse::DebianChangelog adds an additional space to the # beginning of each line, so we have to adjust for that in the # length check. my @lines = split(/\n/, $changes); # real start my $position = $real_start; for my $line (@lines) { my $pointer = $changelog_item->pointer($position); if ($line =~ /^ [*]\s(.{1,5})$/) { my $excerpt = $1; $self->pointed_hint('debian-changelog-line-too-short', $pointer, $excerpt) unless $1 =~ /:$/; } $self->pointed_hint('debian-changelog-line-too-long', $pointer) if length $line >= $MAXIMUM_WIDTH && $line !~ /^ [\s.o*+-]* (?: [Ss]ee:?\s+ )? \S+ $/msx; } continue { ++$position; } # Strip out all lines that contain the word spelling to avoid false # positives on changelog entries for spelling fixes. $changes =~ s/^.*(?:spelling|typo).*\n//gm; check_spelling( $self->data, $changes, $group->spelling_exceptions, $self->spelling_tag_emitter( 'spelling-error-in-changelog', $changelog_item ) ); } return; } # read the changelog itself and check for some issues we cannot find # with Parse::DebianChangelog. Also return the "real" line number for # the first line of text in the first entry. # sub check_dch { my ($self) = @_; my $unresolved = $self->processable->changelog_item; # stop for dangling symbolic link my $item = $unresolved->resolve_path; return 0 unless defined $item; # return empty changelog return 0 unless $item->is_file && $item->is_open_ok; # emacs only looks at the last "local variables" in a file, and only at # one within 3000 chars of EOF and on the last page (^L), but that's a bit # pesky to replicate. Demanding a match of $prefix and $suffix ought to # be enough to avoid false positives. my $contents; if ($item->basename =~ m{ [.]gz $}x) { my $bytes = safe_qx('gunzip', '-c', $item->unpacked_path); return 0 unless valid_utf8($bytes); $contents = decode_utf8($bytes); } else { # empty unless valis UTF-8 $contents = $item->decoded_utf8; } my @lines = split(m{\n}, $contents); my $prefix; my $suffix; my $real_start = 0; my $saw_tab_lead = 0; my $position = 1; for my $line (@lines) { ++$real_start unless $saw_tab_lead; $saw_tab_lead = 1 if $line =~ /^\s+\S/; my $pointer = $item->pointer($position); if ( $line =~ m{ closes: \s* (( (?:bug)? [#]? \s? \d*) [[:alpha:]] \w*) }ix || $line =~ m{ closes: \s* (?:bug)? [#]? \s? \d+ (?: , \s* (?:bug)? [#]? \s? \d+ )* (?: , \s* (( (?:bug)? [#]? \s? \d* ) [[:alpha:]] \w*)) }ix ) { my $bug = $1; $self->pointed_hint('wrong-bug-number-in-closes', $pointer, $bug) if length $2; } if ($line =~ /^(.*)Local\ variables:(.*)$/i) { $prefix = $1; $suffix = $2; } # emacs allows whitespace between prefix and variable, hence \s* $self->pointed_hint( 'debian-changelog-file-contains-obsolete-user-emacs-settings', $pointer) if defined $prefix && defined $suffix && $line =~ /^\Q$prefix\E\s*add-log-mailing-address:.*\Q$suffix\E$/; } continue { ++$position; } return $real_start; } 1; # Local Variables: # indent-tabs-mode: nil # cperl-indent-level: 4 # End: # vim: syntax=perl sw=4 sts=4 sr et