diff options
author | rillig <rillig@pkgsrc.org> | 2015-10-11 19:20:17 +0000 |
---|---|---|
committer | rillig <rillig@pkgsrc.org> | 2015-10-11 19:20:17 +0000 |
commit | 6b0c2cddf692c97bf92afdcecdcc94ffe4ab8f8f (patch) | |
tree | a0c4d52a9403e97df9a8667c95e702cc672c451f /pkgtools/pkglint | |
parent | abfd25a91e25740849b233135f88641ac4f28fc7 (diff) | |
download | pkgsrc-6b0c2cddf692c97bf92afdcecdcc94ffe4ab8f8f.tar.gz |
Moved the code for checking patch files into its own file
The pkglint.pl file has grown pretty large, which makes it hard to under-
stand. Therefore it is useful to split it into reasonable parts.
The code for checking patch files is such a part since it is independent
of the rest of pkglint, and its specialized routines take quite a bit
of space.
Diffstat (limited to 'pkgtools/pkglint')
-rw-r--r-- | pkgtools/pkglint/files/PkgLint/Patches.pm | 637 | ||||
-rw-r--r-- | pkgtools/pkglint/files/build.pl | 6 | ||||
-rw-r--r-- | pkgtools/pkglint/files/pkglint.pl | 654 |
3 files changed, 652 insertions, 645 deletions
diff --git a/pkgtools/pkglint/files/PkgLint/Patches.pm b/pkgtools/pkglint/files/PkgLint/Patches.pm new file mode 100644 index 00000000000..23b7bb8487d --- /dev/null +++ b/pkgtools/pkglint/files/PkgLint/Patches.pm @@ -0,0 +1,637 @@ +# $NetBSD: Patches.pm,v 1.1 2015/10/11 19:20:17 rillig Exp $ +# +# Everything concerning checks for patch files. +# + +# Guess the type of file based on the filename. This is used to select +# the proper subroutine for detecting absolute pathnames. +# +# Returns one of "source", "shell", "make", "text", "configure", +# "ignore", "unknown". +# +sub get_filetype($$) { + my ($line, $fname) = @_; + my $basename = basename($fname); + + # The trailig .in part is not needed, since it does not + # influence the type of contents. + $basename =~ s,\.in$,,; + + # Let's assume that everything else that looks like a Makefile + # is indeed a Makefile. + if ($basename =~ m"^I?[Mm]akefile(?:\..*|)?|.*\.ma?k$") { + return "make"; + } + + # Too many false positives for shell scripts, so configure + # scripts get their own category. + if ($basename =~ m"^configure(?:|\.ac)$") { + $opt_debug_unchecked and $line->log_debug("Skipped check for absolute pathnames."); + return "configure"; + } + + if ($basename =~ m"\.(?:sh|m4)$"i) { + return "shell"; + } + + if ($basename =~ m"\.(?:cc?|cpp|cxx|el|hh?|hpp|l|pl|pm|py|s|t|y)$"i) { + return "source"; + } + + if ($basename =~ m"^.+\.(?:\d+|conf|html|info|man|po|tex|texi|texinfo|txt|xml)$"i) { + return "text"; + } + + # Filenames without extension are hard to guess right. :( + if ($basename !~ m"\.") { + return "unknown"; + } + + $opt_debug_misc and $line->log_debug("Don't know the file type of ${fname}."); + + return "unknown"; +} + +sub checkline_cpp_macro_names($$) { + my ($line, $text) = @_; + my ($rest); + + use constant good_macros => PkgLint::Util::array_to_hash(qw( + __STDC__ + + __GNUC__ __GNUC_MINOR__ + __SUNPRO_C + + __i386 + __mips + __sparc + + __APPLE__ + __bsdi__ + __CYGWIN__ + __DragonFly__ + __FreeBSD__ __FreeBSD_version + __INTERIX + __linux__ + __MINGW32__ + __NetBSD__ __NetBSD_Version__ + __OpenBSD__ + __SVR4 + __sgi + __sun + + __GLIBC__ + )); + use constant bad_macros => { + "__sgi__" => "__sgi", + "__sparc__" => "__sparc", + "__sparc_v9__" => "__sparcv9", + "__sun__" => "__sun", + "__svr4__" => "__SVR4", + }; + + $rest = $text; + while ($rest =~ s/defined\((__[\w_]+)\)// || $rest =~ s/\b(_\w+)\(//) { + my ($macro) = ($1); + + if (exists(good_macros->{$macro})) { + $opt_debug_misc and $line->log_debug("Found good macro \"${macro}\"."); + } elsif (exists(bad_macros->{$macro})) { + $line->log_warning("The macro \"${macro}\" is not portable enough. Please use \"".bad_macros->{$macro}."\" instead."); + $line->explain_warning("See the pkgsrc guide, section \"CPP defines\" for details."); + + } elsif ($macro eq "__NetBSD_Prereq__") { + $line->log_warning("Please use __NetBSD_Version__ instead of __NetBSD_Prereq__."); + $line->explain_warning( +"The __NetBSD_Prereq__ macro is pretty new. It was born in NetBSD", +"4.99.3, and maybe it won't survive for long. A better (and compatible)", +"way is to compare __NetBSD_Version__ directly to the required version", +"number."); + + } elsif ($macro =~ m"^_+NetBSD_+Version_+$"i && $macro ne "__NetBSD_Version__") { + $line->log_warning("Misspelled variant \"${macro}\" of \"__NetBSD_Version__\"."); + + } else { + $opt_debug_unchecked and $line->log_debug("Unchecked macro \"${macro}\"."); + } + } +} + +# Checks whether the line contains text that looks like absolute +# pathnames, assuming that the file uses the common syntax with +# single or double quotes to represent strings. +# +sub checkline_source_absolute_pathname($$) { + my ($line, $text) = @_; + my ($abspath); + + $opt_debug_trace and $line->log_debug("checkline_source_absolute_pathname(${text})"); + + if ($text =~ m"(.*)([\"'])(/[^\"']*)\2") { + my ($before, $delim, $string) = ($1, $2, $3); + + $opt_debug_misc and $line->log_debug("checkline_source_absolute_pathname(before=${before}, string=${string})"); + if ($before =~ m"[A-Z_]+\s*$") { + # allowed: PREFIX "/bin/foo" + + } elsif ($string =~ m"^/[*/]") { + # This is more likely to be a C or C++ comment. + + } elsif ($string !~ m"^/\w") { + # Assume that pathnames start with a letter or digit. + + } elsif ($before =~ m"\+\s*$") { + # Something like foodir + '/lib' + + } else { + $abspath = $string; + } + } + + if (defined($abspath)) { + checkword_absolute_pathname($line, $abspath); + } +} + +# Last resort if the file does not look like a Makefile or typical +# source code. All strings that look like pathnames and start with +# one of the typical Unix prefixes are found. +# +sub checkline_other_absolute_pathname($$) { + my ($line, $text) = @_; + + $opt_debug_trace and $line->log_debug("checkline_other_absolute_pathname(\"${text}\")"); + + if ($text =~ m"^#[^!]") { + # Don't warn for absolute pathnames in comments, + # except for shell interpreters. + + } elsif ($text =~ m"^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$") { + my ($before, $path, $after) = ($1, $2, $3); + + if ($before =~ m"\@$") { + # Something like @PREFIX@/bin + + } elsif ($before =~ m"[)}]$") { + # Something like ${prefix}/bin or $(PREFIX)/bin + + } elsif ($before =~ m"\+\s*[\"']$") { + # Something like foodir + '/lib' + + } elsif ($before =~ m"\w$") { + # Something like $dir/lib + + } elsif ($before =~ m"\.$") { + # ../foo is not an absolute pathname. + + } else { + $opt_debug_misc and $line->log_debug("before=${before}"); + checkword_absolute_pathname($line, $path); + } + } +} + +sub checkfile_patch($) { + my ($fname) = @_; + my ($lines); + my ($state, $redostate, $nextstate, $dellines, $addlines, $hunks); + my ($seen_comment, $current_fname, $current_ftype, $patched_files); + my ($leading_context_lines, $trailing_context_lines, $context_scanning_leading); + + # Abbreviations used: + # style: [c] = context diff, [u] = unified diff + # scope: [f] = file, [h] = hunk, [l] = line + # action: [d] = delete, [m] = modify, [a] = add, [c] = context + use constant re_patch_rcsid => qr"^\$.*\$$"; + use constant re_patch_text => qr"^(.+)$"; + use constant re_patch_empty => qr"^$"; + use constant re_patch_cfd => qr"^\*\*\*\s(\S+)(.*)$"; + use constant re_patch_cfa => qr"^---\s(\S+)(.*)$"; + use constant re_patch_ch => qr"^\*{15}(.*)$"; + use constant re_patch_chd => qr"^\*{3}\s(\d+)(?:,(\d+))?\s\*{4}$"; + use constant re_patch_cha => qr"^-{3}\s(\d+)(?:,(\d+))?\s-{4}$"; + use constant re_patch_cld => qr"^(?:-\s(.*))?$"; + use constant re_patch_clm => qr"^(?:!\s(.*))?$"; + use constant re_patch_cla => qr"^(?:\+\s(.*))?$"; + use constant re_patch_clc => qr"^(?:\s\s(.*))?$"; + use constant re_patch_ufd => qr"^---\s(\S+)(?:\s+(.*))?$"; + use constant re_patch_ufa => qr"^\+{3}\s(\S+)(?:\s+(.*))?$"; + use constant re_patch_uh => qr"^\@\@\s-(?:(\d+),)?(\d+)\s\+(?:(\d+),)?(\d+)\s\@\@(.*)$"; + use constant re_patch_uld => qr"^-(.*)$"; + use constant re_patch_ula => qr"^\+(.*)$"; + use constant re_patch_ulc => qr"^\s(.*)$"; + use constant re_patch_ulnonl => qr"^\\ No newline at end of file$"; + + use enum qw(:PST_ + START CENTER TEXT + CFA CH CHD CLD0 CLD CLA0 CLA + UFA UH UL + ); + + my @comment_explanation = ( +"Each patch must document why it is necessary. If it has been applied", +"because of a security issue, a reference to the CVE should be mentioned", +"as well.", +"", +"Since it is our goal to have as few patches as possible, all patches", +"should be sent to the upstream maintainers of the package. After you", +"have done so, you should add a reference to the bug report containing", +"the patch."); + + my ($line, $m); + + my $check_text = sub($) { + my ($text) = @_; + + if ($text =~ m"(\$(Author|Date|Header|Id|Locker|Log|Name|RCSfile|Revision|Source|State|$opt_rcsidstring)(?::[^\$]*)?\$)") { + my ($tag) = ($2); + + if ($text =~ re_patch_uh) { + $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it."); + $line->set_text($1); + } else { + $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\"."); + } + } + }; + + my $check_contents = sub() { + + if ($m->has(1)) { + $check_text->($m->text(1)); + } + }; + + my $check_added_contents = sub() { + my $text; + + return unless $m->has(1); + $text = $m->text(1); + checkline_cpp_macro_names($line, $text); + checkline_spellcheck($line); + + # XXX: This check is not as accurate as the similar one in + # checkline_mk_shelltext(). + if (defined($current_fname)) { + if ($current_ftype eq "shell" || $current_ftype eq "make") { + my ($mm, $rest) = match_all($text, $regex_shellword); + + foreach my $m (@{$mm}) { + my $shellword = $m->text(1); + + if ($shellword =~ m"^#") { + last; + } + checkline_mk_absolute_pathname($line, $shellword); + } + + } elsif ($current_ftype eq "source") { + checkline_source_absolute_pathname($line, $text); + + } elsif ($current_ftype eq "configure") { + if ($text =~ m": Avoid regenerating within pkgsrc$") { + $line->log_error("This code must not be included in patches."); + $line->explain_error( +"It is generated automatically by pkgsrc after the patch phase.", +"", +"For more details, look for \"configure-scripts-override\" in", +"mk/configure/gnu-configure.mk."); + } + + } elsif ($current_ftype eq "ignore") { + # Ignore it. + + } else { + checkline_other_absolute_pathname($line, $text); + } + } + }; + + my $check_hunk_end = sub($$$) { + my ($deldelta, $adddelta, $newstate) = @_; + + if ($deldelta > 0 && $dellines == 0) { + $redostate = $newstate; + if (defined($addlines) && $addlines > 0) { + $line->log_error("Expected ${addlines} more lines to be added."); + } + } elsif ($adddelta > 0 && $addlines == 0) { + $redostate = $newstate; + if (defined($dellines) && $dellines > 0) { + $line->log_error("Expected ${dellines} more lines to be deleted."); + } + } else { + if (defined($context_scanning_leading)) { + if ($deldelta != 0 && $adddelta != 0) { + if ($context_scanning_leading) { + $leading_context_lines++; + } else { + $trailing_context_lines++; + } + } else { + if ($context_scanning_leading) { + $context_scanning_leading = false; + } else { + $trailing_context_lines = 0; + } + } + } + + if ($deldelta != 0) { + $dellines -= $deldelta; + } + if ($adddelta != 0) { + $addlines -= $adddelta; + } + if (!((defined($dellines) && $dellines > 0) || + (defined($addlines) && $addlines > 0))) { + if (defined($context_scanning_leading)) { + if ($leading_context_lines != $trailing_context_lines) { + $opt_debug_patches and $line->log_warning("The hunk that ends here does not have as many leading (${leading_context_lines}) as trailing (${trailing_context_lines}) lines of context."); + } + } + $nextstate = $newstate; + } + } + }; + + # @param deldelta + # The number of lines that are deleted from the patched file. + # @param adddelta + # The number of lines that are added to the patched file. + # @param newstate + # The follow-up state when this line is the last line to be + # added in this hunk of the patch. + # + my $check_hunk_line = sub($$$) { + my ($deldelta, $adddelta, $newstate) = @_; + + $check_contents->(); + $check_hunk_end->($deldelta, $adddelta, $newstate); + + # If -Wextra is given, the context lines are checked for + # absolute paths and similar things. If it is not given, + # only those lines that really add something to the patched + # file are checked. + if ($adddelta != 0 && ($deldelta == 0 || $opt_warn_extra)) { + $check_added_contents->(); + } + }; + + # [ regex, to state, action ] + my $transitions = { + PST_START() => + [ [re_patch_rcsid, PST_CENTER, sub() { + checkline_rcsid($line, ""); + }], [undef, PST_CENTER, sub() { + checkline_rcsid($line, ""); + }]], + PST_CENTER() => + [ [re_patch_empty, PST_TEXT, sub() { + # + }], [re_patch_cfd, PST_CFA, sub() { + if ($seen_comment) { + $opt_warn_space and $line->log_note("Empty line expected."); + } else { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + $line->log_warning("Please use unified diffs (diff -u) for patches."); + }], [re_patch_ufd, PST_UFA, sub() { + if ($seen_comment) { + $opt_warn_space and $line->log_note("Empty line expected."); + } else { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + }], [undef, PST_TEXT, sub() { + $opt_warn_space and $line->log_note("Empty line expected."); + }]], + PST_TEXT() => + [ [re_patch_cfd, PST_CFA, sub() { + if (!$seen_comment) { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + $line->log_warning("Please use unified diffs (diff -u) for patches."); + }], [re_patch_ufd, PST_UFA, sub() { + if (!$seen_comment) { + $line->log_error("Comment expected."); + $line->explain_error(@comment_explanation); + } + }], [re_patch_text, PST_TEXT, sub() { + $seen_comment = true; + }], [re_patch_empty, PST_TEXT, sub() { + # + }], [undef, PST_TEXT, sub() { + # + }]], + PST_CFA() => + [ [re_patch_cfa, PST_CH, sub() { + $current_fname = $m->text(1); + $current_ftype = get_filetype($line, $current_fname); + $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); + $patched_files++; + $hunks = 0; + }]], + PST_CH() => + [ [re_patch_ch, PST_CHD, sub() { + $hunks++; + }]], + PST_CHD() => + [ [re_patch_chd, PST_CLD0, sub() { + $dellines = ($m->has(2)) + ? (1 + $m->text(2) - $m->text(1)) + : ($m->text(1)); + }]], + PST_CLD0() => + [ [re_patch_clc, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cld, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_clm, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cha, PST_CLA0, sub() { + $dellines = undef; + $addlines = ($m->has(2)) + ? (1 + $m->text(2) - $m->text(1)) + : ($m->text(1)); + }]], + PST_CLD() => + [ [re_patch_clc, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_cld, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [re_patch_clm, PST_CLD, sub() { + $check_hunk_line->(1, 0, PST_CLD0); + }], [undef, PST_CLD0, sub() { + if ($dellines != 0) { + $line->log_warning("Invalid number of deleted lines (${dellines} missing)."); + } + }]], + PST_CLA0() => + [ [re_patch_clc, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_clm, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_cla, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [undef, PST_CH, sub() { + # + }]], + PST_CLA() => + [ [re_patch_clc, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_clm, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [re_patch_cla, PST_CLA, sub() { + $check_hunk_line->(0, 1, PST_CH); + }], [undef, PST_CLA0, sub() { + if ($addlines != 0) { + $line->log_warning("Invalid number of added lines (${addlines} missing)."); + } + }]], + PST_CH() => + [ [undef, PST_TEXT, sub() { + # + }]], + PST_UFA() => + [ [re_patch_ufa, PST_UH, sub() { + $current_fname = $m->text(1); + $current_ftype = get_filetype($line, $current_fname); + $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); + $patched_files++; + $hunks = 0; + }]], + PST_UH() => + [ [re_patch_uh, PST_UL, sub() { + $dellines = ($m->has(1) ? $m->text(2) : 1); + $addlines = ($m->has(3) ? $m->text(4) : 1); + $check_text->($line->text); + if ($line->text =~ m"\r$") { + $line->log_error("The hunk header must not end with a CR character."); + $line->explain_error( +"The MacOS X patch utility cannot handle these."); + } + $hunks++; + $context_scanning_leading = (($m->has(1) && $m->text(1) ne "1") ? true : undef); + $leading_context_lines = 0; + $trailing_context_lines = 0; + }], [undef, PST_TEXT, sub() { + ($hunks != 0) || $line->log_warning("No hunks for file ${current_fname}."); + }]], + PST_UL() => + [ [re_patch_uld, PST_UL, sub() { + $check_hunk_line->(1, 0, PST_UH); + }], [re_patch_ula, PST_UL, sub() { + $check_hunk_line->(0, 1, PST_UH); + }], [re_patch_ulc, PST_UL, sub() { + $check_hunk_line->(1, 1, PST_UH); + }], [re_patch_ulnonl, PST_UL, sub() { + # + }], [re_patch_empty, PST_UL, sub() { + $opt_warn_space and $line->log_note("Leading white-space missing in hunk."); + $check_hunk_line->(1, 1, PST_UH); + }], [undef, PST_UH, sub() { + if ($dellines != 0 || $addlines != 0) { + $line->log_warning("Unexpected end of hunk (-${dellines},+${addlines} expected)."); + } + }]]}; + + $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_patch()"); + + checkperms($fname); + if (!($lines = load_lines($fname, false))) { + log_error($fname, NO_LINE_NUMBER, "Could not be read."); + return; + } + if (@{$lines} == 0) { + log_error($fname, NO_LINE_NUMBER, "Must not be empty."); + return; + } + + $state = PST_START; + $dellines = undef; + $addlines = undef; + $patched_files = 0; + $seen_comment = false; + $current_fname = undef; + $current_ftype = undef; + $hunks = undef; + + for (my $lineno = 0; $lineno <= $#{$lines}; ) { + $line = $lines->[$lineno]; + my $text = $line->text; + + $opt_debug_patches and $line->log_debug("[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."] $text"); + + my $found = false; + foreach my $t (@{$transitions->{$state}}) { + if (!defined($t->[0])) { + $m = undef; + } elsif ($text =~ $t->[0]) { + $opt_debug_patches and $line->log_debug($t->[0]); + $m = PkgLint::SimpleMatch->new($text, \@-, \@+); + } else { + next; + } + $redostate = undef; + $nextstate = $t->[1]; + $t->[2]->(); + if (defined($redostate)) { + $state = $redostate; + } else { + $state = $nextstate; + if (defined($t->[0])) { + $lineno++; + } + } + $found = true; + last; + } + + if (!$found) { + $line->log_error("Parse error: state=${state}"); + $state = PST_TEXT; + $lineno++; + } + } + + while ($state != PST_TEXT) { + $opt_debug_patches and log_debug($fname, "EOF", "[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."]"); + + my $found = false; + foreach my $t (@{$transitions->{$state}}) { + if (!defined($t->[0])) { + my $newstate; + + $m = undef; + $redostate = undef; + $nextstate = $t->[1]; + $t->[2]->(); + $newstate = (defined($redostate)) ? $redostate : $nextstate; + if ($newstate == $state) { + log_fatal($fname, "EOF", "Internal error in the patch transition table."); + } + $state = $newstate; + $found = true; + last; + } + } + + if (!$found) { + log_error($fname, "EOF", "Parse error: state=${state}"); + $state = PST_TEXT; + } + } + + if ($patched_files > 1) { + log_warning($fname, NO_LINE_NUMBER, "Contains patches for $patched_files files, should be only one."); + + } elsif ($patched_files == 0) { + log_error($fname, NO_LINE_NUMBER, "Contains no patch."); + } + + checklines_trailing_empty_lines($lines); +} + diff --git a/pkgtools/pkglint/files/build.pl b/pkgtools/pkglint/files/build.pl index 97caff3b4c0..21de9a25538 100644 --- a/pkgtools/pkglint/files/build.pl +++ b/pkgtools/pkglint/files/build.pl @@ -1,5 +1,5 @@ #! @PERL@ -# $NetBSD: build.pl,v 1.1 2013/03/26 15:08:29 schmonz Exp $ +# $NetBSD: build.pl,v 1.2 2015/10/11 19:20:17 rillig Exp $ # use strict; @@ -17,8 +17,8 @@ sub readfile { } while (my $line = <>) { - if ($line =~ /^use PkgLint::(.+);$/) { - print readfile("PkgLint/$1.pm"); + if ($line =~ m"^#include (.+)$/") { + print readfile($1); } else { print $line; } diff --git a/pkgtools/pkglint/files/pkglint.pl b/pkgtools/pkglint/files/pkglint.pl index 2170f56fb43..ce575495097 100644 --- a/pkgtools/pkglint/files/pkglint.pl +++ b/pkgtools/pkglint/files/pkglint.pl @@ -1,5 +1,5 @@ #! @PERL@ -# $NetBSD: pkglint.pl,v 1.886 2015/10/11 18:42:34 rillig Exp $ +# $NetBSD: pkglint.pl,v 1.887 2015/10/11 19:20:17 rillig Exp $ # # pkglint - static analyzer and checker for pkgsrc packages @@ -34,16 +34,16 @@ use v5.12; use warnings; -use PkgLint::Util; -use PkgLint::Logging; -use PkgLint::SimpleMatch; -use PkgLint::Line; -use PkgLint::FileUtil; -use PkgLint::Type; -use PkgLint::VarUseContext; -use PkgLint::SubstContext; -use PkgLint::CVS_Entry; -use PkgLint::Change; +#include PkgLint/Util.pm +#include PkgLint/Logging.pm +#include PkgLint/SimpleMatch.pm +#include PkgLint/Line.pm +#include PkgLint/FileUtil.pm +#include PkgLint/Type.pm +#include PkgLint/VarUseContext.pm +#include PkgLint/SubstContext.pm +#include PkgLint/CVS_Entry.pm +#include PkgLint/Change.pm package pkglint; #========================================================================== @@ -1368,55 +1368,6 @@ sub is_emptydir($) { return $rv; } -# Guess the type of file based on the filename. This is used to select -# the proper subroutine for detecting absolute pathnames. -# -# Returns one of "source", "shell", "make", "text", "configure", -# "ignore", "unknown". -# -sub get_filetype($$) { - my ($line, $fname) = @_; - my $basename = basename($fname); - - # The trailig .in part is not needed, since it does not - # influence the type of contents. - $basename =~ s,\.in$,,; - - # Let's assume that everything else that looks like a Makefile - # is indeed a Makefile. - if ($basename =~ m"^I?[Mm]akefile(?:\..*|)?|.*\.ma?k$") { - return "make"; - } - - # Too many false positives for shell scripts, so configure - # scripts get their own category. - if ($basename =~ m"^configure(?:|\.ac)$") { - $opt_debug_unchecked and $line->log_debug("Skipped check for absolute pathnames."); - return "configure"; - } - - if ($basename =~ m"\.(?:sh|m4)$"i) { - return "shell"; - } - - if ($basename =~ m"\.(?:cc?|cpp|cxx|el|hh?|hpp|l|pl|pm|py|s|t|y)$"i) { - return "source"; - } - - if ($basename =~ m"^.+\.(?:\d+|conf|html|info|man|po|tex|texi|texinfo|txt|xml)$"i) { - return "text"; - } - - # Filenames without extension are hard to guess right. :( - if ($basename !~ m"\.") { - return "unknown"; - } - - $opt_debug_misc and $line->log_debug("Don't know the file type of ${fname}."); - - return "unknown"; -} - # Returns the list of subdirectories of a directory, except "CVS". sub get_subdirs($) { my ($dir) = @_; @@ -2537,42 +2488,6 @@ sub checkline_rcsid($$) { checkline_rcsid_regex($line, quotemeta($prefix), $prefix); } -# Checks whether the line contains text that looks like absolute -# pathnames, assuming that the file uses the common syntax with -# single or double quotes to represent strings. -# -sub checkline_source_absolute_pathname($$) { - my ($line, $text) = @_; - my ($abspath); - - $opt_debug_trace and $line->log_debug("checkline_source_absolute_pathname(${text})"); - - if ($text =~ m"(.*)([\"'])(/[^\"']*)\2") { - my ($before, $delim, $string) = ($1, $2, $3); - - $opt_debug_misc and $line->log_debug("checkline_source_absolute_pathname(before=${before}, string=${string})"); - if ($before =~ m"[A-Z_]+\s*$") { - # allowed: PREFIX "/bin/foo" - - } elsif ($string =~ m"^/[*/]") { - # This is more likely to be a C or C++ comment. - - } elsif ($string !~ m"^/\w") { - # Assume that pathnames start with a letter or digit. - - } elsif ($before =~ m"\+\s*$") { - # Something like foodir + '/lib' - - } else { - $abspath = $string; - } - } - - if (defined($abspath)) { - checkword_absolute_pathname($line, $abspath); - } -} - sub checkline_mk_absolute_pathname($$) { my ($line, $text) = @_; my $abspath; @@ -2598,44 +2513,6 @@ sub checkline_mk_absolute_pathname($$) { } } -# Last resort if the file does not look like a Makefile or typical -# source code. All strings that look like pathnames and start with -# one of the typical Unix prefixes are found. -# -sub checkline_other_absolute_pathname($$) { - my ($line, $text) = @_; - - $opt_debug_trace and $line->log_debug("checkline_other_absolute_pathname(\"${text}\")"); - - if ($text =~ m"^#[^!]") { - # Don't warn for absolute pathnames in comments, - # except for shell interpreters. - - } elsif ($text =~ m"^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$") { - my ($before, $path, $after) = ($1, $2, $3); - - if ($before =~ m"\@$") { - # Something like @PREFIX@/bin - - } elsif ($before =~ m"[)}]$") { - # Something like ${prefix}/bin or $(PREFIX)/bin - - } elsif ($before =~ m"\+\s*[\"']$") { - # Something like foodir + '/lib' - - } elsif ($before =~ m"\w$") { - # Something like $dir/lib - - } elsif ($before =~ m"\.$") { - # ../foo is not an absolute pathname. - - } else { - $opt_debug_misc and $line->log_debug("before=${before}"); - checkword_absolute_pathname($line, $path); - } - } -} - sub checkline_relative_path($$$) { my ($line, $path, $must_exist) = @_; my ($res_path); @@ -2689,71 +2566,6 @@ sub checkline_spellcheck($) { } } -sub checkline_cpp_macro_names($$) { - my ($line, $text) = @_; - my ($rest); - - use constant good_macros => PkgLint::Util::array_to_hash(qw( - __STDC__ - - __GNUC__ __GNUC_MINOR__ - __SUNPRO_C - - __i386 - __mips - __sparc - - __APPLE__ - __bsdi__ - __CYGWIN__ - __DragonFly__ - __FreeBSD__ __FreeBSD_version - __INTERIX - __linux__ - __MINGW32__ - __NetBSD__ __NetBSD_Version__ - __OpenBSD__ - __SVR4 - __sgi - __sun - - __GLIBC__ - )); - use constant bad_macros => { - "__sgi__" => "__sgi", - "__sparc__" => "__sparc", - "__sparc_v9__" => "__sparcv9", - "__sun__" => "__sun", - "__svr4__" => "__SVR4", - }; - - $rest = $text; - while ($rest =~ s/defined\((__[\w_]+)\)// || $rest =~ s/\b(_\w+)\(//) { - my ($macro) = ($1); - - if (exists(good_macros->{$macro})) { - $opt_debug_misc and $line->log_debug("Found good macro \"${macro}\"."); - } elsif (exists(bad_macros->{$macro})) { - $line->log_warning("The macro \"${macro}\" is not portable enough. Please use \"".bad_macros->{$macro}."\" instead."); - $line->explain_warning("See the pkgsrc guide, section \"CPP defines\" for details."); - - } elsif ($macro eq "__NetBSD_Prereq__") { - $line->log_warning("Please use __NetBSD_Version__ instead of __NetBSD_Prereq__."); - $line->explain_warning( -"The __NetBSD_Prereq__ macro is pretty new. It was born in NetBSD", -"4.99.3, and maybe it won't survive for long. A better (and compatible)", -"way is to compare __NetBSD_Version__ directly to the required version", -"number."); - - } elsif ($macro =~ m"^_+NetBSD_+Version_+$"i && $macro ne "__NetBSD_Version__") { - $line->log_warning("Misspelled variant \"${macro}\" of \"__NetBSD_Version__\"."); - - } else { - $opt_debug_unchecked and $line->log_debug("Unchecked macro \"${macro}\"."); - } - } -} - sub checkline_mk_varuse($$$$) { my ($line, $varname, $mod, $context) = @_; @@ -6055,449 +5867,7 @@ sub checkfile_package_Makefile($$) { autofix($lines); } -sub checkfile_patch($) { - my ($fname) = @_; - my ($lines); - my ($state, $redostate, $nextstate, $dellines, $addlines, $hunks); - my ($seen_comment, $current_fname, $current_ftype, $patched_files); - my ($leading_context_lines, $trailing_context_lines, $context_scanning_leading); - - # Abbreviations used: - # style: [c] = context diff, [u] = unified diff - # scope: [f] = file, [h] = hunk, [l] = line - # action: [d] = delete, [m] = modify, [a] = add, [c] = context - use constant re_patch_rcsid => qr"^\$.*\$$"; - use constant re_patch_text => qr"^(.+)$"; - use constant re_patch_empty => qr"^$"; - use constant re_patch_cfd => qr"^\*\*\*\s(\S+)(.*)$"; - use constant re_patch_cfa => qr"^---\s(\S+)(.*)$"; - use constant re_patch_ch => qr"^\*{15}(.*)$"; - use constant re_patch_chd => qr"^\*{3}\s(\d+)(?:,(\d+))?\s\*{4}$"; - use constant re_patch_cha => qr"^-{3}\s(\d+)(?:,(\d+))?\s-{4}$"; - use constant re_patch_cld => qr"^(?:-\s(.*))?$"; - use constant re_patch_clm => qr"^(?:!\s(.*))?$"; - use constant re_patch_cla => qr"^(?:\+\s(.*))?$"; - use constant re_patch_clc => qr"^(?:\s\s(.*))?$"; - use constant re_patch_ufd => qr"^---\s(\S+)(?:\s+(.*))?$"; - use constant re_patch_ufa => qr"^\+{3}\s(\S+)(?:\s+(.*))?$"; - use constant re_patch_uh => qr"^\@\@\s-(?:(\d+),)?(\d+)\s\+(?:(\d+),)?(\d+)\s\@\@(.*)$"; - use constant re_patch_uld => qr"^-(.*)$"; - use constant re_patch_ula => qr"^\+(.*)$"; - use constant re_patch_ulc => qr"^\s(.*)$"; - use constant re_patch_ulnonl => qr"^\\ No newline at end of file$"; - - use enum qw(:PST_ - START CENTER TEXT - CFA CH CHD CLD0 CLD CLA0 CLA - UFA UH UL - ); - - my @comment_explanation = ( -"Each patch must document why it is necessary. If it has been applied", -"because of a security issue, a reference to the CVE should be mentioned", -"as well.", -"", -"Since it is our goal to have as few patches as possible, all patches", -"should be sent to the upstream maintainers of the package. After you", -"have done so, you should add a reference to the bug report containing", -"the patch."); - - my ($line, $m); - - my $check_text = sub($) { - my ($text) = @_; - - if ($text =~ m"(\$(Author|Date|Header|Id|Locker|Log|Name|RCSfile|Revision|Source|State|$opt_rcsidstring)(?::[^\$]*)?\$)") { - my ($tag) = ($2); - - if ($text =~ re_patch_uh) { - $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it."); - $line->set_text($1); - } else { - $line->log_warning("Found RCS tag \"\$${tag}\$\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\"."); - } - } - }; - - my $check_contents = sub() { - - if ($m->has(1)) { - $check_text->($m->text(1)); - } - }; - - my $check_added_contents = sub() { - my $text; - - return unless $m->has(1); - $text = $m->text(1); - checkline_cpp_macro_names($line, $text); - checkline_spellcheck($line); - - # XXX: This check is not as accurate as the similar one in - # checkline_mk_shelltext(). - if (defined($current_fname)) { - if ($current_ftype eq "shell" || $current_ftype eq "make") { - my ($mm, $rest) = match_all($text, $regex_shellword); - - foreach my $m (@{$mm}) { - my $shellword = $m->text(1); - - if ($shellword =~ m"^#") { - last; - } - checkline_mk_absolute_pathname($line, $shellword); - } - - } elsif ($current_ftype eq "source") { - checkline_source_absolute_pathname($line, $text); - - } elsif ($current_ftype eq "configure") { - if ($text =~ m": Avoid regenerating within pkgsrc$") { - $line->log_error("This code must not be included in patches."); - $line->explain_error( -"It is generated automatically by pkgsrc after the patch phase.", -"", -"For more details, look for \"configure-scripts-override\" in", -"mk/configure/gnu-configure.mk."); - } - - } elsif ($current_ftype eq "ignore") { - # Ignore it. - - } else { - checkline_other_absolute_pathname($line, $text); - } - } - }; - - my $check_hunk_end = sub($$$) { - my ($deldelta, $adddelta, $newstate) = @_; - - if ($deldelta > 0 && $dellines == 0) { - $redostate = $newstate; - if (defined($addlines) && $addlines > 0) { - $line->log_error("Expected ${addlines} more lines to be added."); - } - } elsif ($adddelta > 0 && $addlines == 0) { - $redostate = $newstate; - if (defined($dellines) && $dellines > 0) { - $line->log_error("Expected ${dellines} more lines to be deleted."); - } - } else { - if (defined($context_scanning_leading)) { - if ($deldelta != 0 && $adddelta != 0) { - if ($context_scanning_leading) { - $leading_context_lines++; - } else { - $trailing_context_lines++; - } - } else { - if ($context_scanning_leading) { - $context_scanning_leading = false; - } else { - $trailing_context_lines = 0; - } - } - } - - if ($deldelta != 0) { - $dellines -= $deldelta; - } - if ($adddelta != 0) { - $addlines -= $adddelta; - } - if (!((defined($dellines) && $dellines > 0) || - (defined($addlines) && $addlines > 0))) { - if (defined($context_scanning_leading)) { - if ($leading_context_lines != $trailing_context_lines) { - $opt_debug_patches and $line->log_warning("The hunk that ends here does not have as many leading (${leading_context_lines}) as trailing (${trailing_context_lines}) lines of context."); - } - } - $nextstate = $newstate; - } - } - }; - - # @param deldelta - # The number of lines that are deleted from the patched file. - # @param adddelta - # The number of lines that are added to the patched file. - # @param newstate - # The follow-up state when this line is the last line to be - # added in this hunk of the patch. - # - my $check_hunk_line = sub($$$) { - my ($deldelta, $adddelta, $newstate) = @_; - - $check_contents->(); - $check_hunk_end->($deldelta, $adddelta, $newstate); - - # If -Wextra is given, the context lines are checked for - # absolute paths and similar things. If it is not given, - # only those lines that really add something to the patched - # file are checked. - if ($adddelta != 0 && ($deldelta == 0 || $opt_warn_extra)) { - $check_added_contents->(); - } - }; - - # [ regex, to state, action ] - my $transitions = { - PST_START() => - [ [re_patch_rcsid, PST_CENTER, sub() { - checkline_rcsid($line, ""); - }], [undef, PST_CENTER, sub() { - checkline_rcsid($line, ""); - }]], - PST_CENTER() => - [ [re_patch_empty, PST_TEXT, sub() { - # - }], [re_patch_cfd, PST_CFA, sub() { - if ($seen_comment) { - $opt_warn_space and $line->log_note("Empty line expected."); - } else { - $line->log_error("Comment expected."); - $line->explain_error(@comment_explanation); - } - $line->log_warning("Please use unified diffs (diff -u) for patches."); - }], [re_patch_ufd, PST_UFA, sub() { - if ($seen_comment) { - $opt_warn_space and $line->log_note("Empty line expected."); - } else { - $line->log_error("Comment expected."); - $line->explain_error(@comment_explanation); - } - }], [undef, PST_TEXT, sub() { - $opt_warn_space and $line->log_note("Empty line expected."); - }]], - PST_TEXT() => - [ [re_patch_cfd, PST_CFA, sub() { - if (!$seen_comment) { - $line->log_error("Comment expected."); - $line->explain_error(@comment_explanation); - } - $line->log_warning("Please use unified diffs (diff -u) for patches."); - }], [re_patch_ufd, PST_UFA, sub() { - if (!$seen_comment) { - $line->log_error("Comment expected."); - $line->explain_error(@comment_explanation); - } - }], [re_patch_text, PST_TEXT, sub() { - $seen_comment = true; - }], [re_patch_empty, PST_TEXT, sub() { - # - }], [undef, PST_TEXT, sub() { - # - }]], - PST_CFA() => - [ [re_patch_cfa, PST_CH, sub() { - $current_fname = $m->text(1); - $current_ftype = get_filetype($line, $current_fname); - $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); - $patched_files++; - $hunks = 0; - }]], - PST_CH() => - [ [re_patch_ch, PST_CHD, sub() { - $hunks++; - }]], - PST_CHD() => - [ [re_patch_chd, PST_CLD0, sub() { - $dellines = ($m->has(2)) - ? (1 + $m->text(2) - $m->text(1)) - : ($m->text(1)); - }]], - PST_CLD0() => - [ [re_patch_clc, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [re_patch_cld, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [re_patch_clm, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [re_patch_cha, PST_CLA0, sub() { - $dellines = undef; - $addlines = ($m->has(2)) - ? (1 + $m->text(2) - $m->text(1)) - : ($m->text(1)); - }]], - PST_CLD() => - [ [re_patch_clc, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [re_patch_cld, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [re_patch_clm, PST_CLD, sub() { - $check_hunk_line->(1, 0, PST_CLD0); - }], [undef, PST_CLD0, sub() { - if ($dellines != 0) { - $line->log_warning("Invalid number of deleted lines (${dellines} missing)."); - } - }]], - PST_CLA0() => - [ [re_patch_clc, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [re_patch_clm, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [re_patch_cla, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [undef, PST_CH, sub() { - # - }]], - PST_CLA() => - [ [re_patch_clc, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [re_patch_clm, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [re_patch_cla, PST_CLA, sub() { - $check_hunk_line->(0, 1, PST_CH); - }], [undef, PST_CLA0, sub() { - if ($addlines != 0) { - $line->log_warning("Invalid number of added lines (${addlines} missing)."); - } - }]], - PST_CH() => - [ [undef, PST_TEXT, sub() { - # - }]], - PST_UFA() => - [ [re_patch_ufa, PST_UH, sub() { - $current_fname = $m->text(1); - $current_ftype = get_filetype($line, $current_fname); - $opt_debug_patches and $line->log_debug("fname=$current_fname ftype=$current_ftype"); - $patched_files++; - $hunks = 0; - }]], - PST_UH() => - [ [re_patch_uh, PST_UL, sub() { - $dellines = ($m->has(1) ? $m->text(2) : 1); - $addlines = ($m->has(3) ? $m->text(4) : 1); - $check_text->($line->text); - if ($line->text =~ m"\r$") { - $line->log_error("The hunk header must not end with a CR character."); - $line->explain_error( -"The MacOS X patch utility cannot handle these."); - } - $hunks++; - $context_scanning_leading = (($m->has(1) && $m->text(1) ne "1") ? true : undef); - $leading_context_lines = 0; - $trailing_context_lines = 0; - }], [undef, PST_TEXT, sub() { - ($hunks != 0) || $line->log_warning("No hunks for file ${current_fname}."); - }]], - PST_UL() => - [ [re_patch_uld, PST_UL, sub() { - $check_hunk_line->(1, 0, PST_UH); - }], [re_patch_ula, PST_UL, sub() { - $check_hunk_line->(0, 1, PST_UH); - }], [re_patch_ulc, PST_UL, sub() { - $check_hunk_line->(1, 1, PST_UH); - }], [re_patch_ulnonl, PST_UL, sub() { - # - }], [re_patch_empty, PST_UL, sub() { - $opt_warn_space and $line->log_note("Leading white-space missing in hunk."); - $check_hunk_line->(1, 1, PST_UH); - }], [undef, PST_UH, sub() { - if ($dellines != 0 || $addlines != 0) { - $line->log_warning("Unexpected end of hunk (-${dellines},+${addlines} expected)."); - } - }]]}; - - $opt_debug_trace and log_debug($fname, NO_LINES, "checkfile_patch()"); - - checkperms($fname); - if (!($lines = load_lines($fname, false))) { - log_error($fname, NO_LINE_NUMBER, "Could not be read."); - return; - } - if (@{$lines} == 0) { - log_error($fname, NO_LINE_NUMBER, "Must not be empty."); - return; - } - - $state = PST_START; - $dellines = undef; - $addlines = undef; - $patched_files = 0; - $seen_comment = false; - $current_fname = undef; - $current_ftype = undef; - $hunks = undef; - - for (my $lineno = 0; $lineno <= $#{$lines}; ) { - $line = $lines->[$lineno]; - my $text = $line->text; - - $opt_debug_patches and $line->log_debug("[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."] $text"); - - my $found = false; - foreach my $t (@{$transitions->{$state}}) { - if (!defined($t->[0])) { - $m = undef; - } elsif ($text =~ $t->[0]) { - $opt_debug_patches and $line->log_debug($t->[0]); - $m = PkgLint::SimpleMatch->new($text, \@-, \@+); - } else { - next; - } - $redostate = undef; - $nextstate = $t->[1]; - $t->[2]->(); - if (defined($redostate)) { - $state = $redostate; - } else { - $state = $nextstate; - if (defined($t->[0])) { - $lineno++; - } - } - $found = true; - last; - } - - if (!$found) { - $line->log_error("Parse error: state=${state}"); - $state = PST_TEXT; - $lineno++; - } - } - - while ($state != PST_TEXT) { - $opt_debug_patches and log_debug($fname, "EOF", "[${state} ${patched_files}/".($hunks||0)."/-".($dellines||0)."+".($addlines||0)."]"); - - my $found = false; - foreach my $t (@{$transitions->{$state}}) { - if (!defined($t->[0])) { - my $newstate; - - $m = undef; - $redostate = undef; - $nextstate = $t->[1]; - $t->[2]->(); - $newstate = (defined($redostate)) ? $redostate : $nextstate; - if ($newstate == $state) { - log_fatal($fname, "EOF", "Internal error in the patch transition table."); - } - $state = $newstate; - $found = true; - last; - } - } - - if (!$found) { - log_error($fname, "EOF", "Parse error: state=${state}"); - $state = PST_TEXT; - } - } - - if ($patched_files > 1) { - log_warning($fname, NO_LINE_NUMBER, "Contains patches for $patched_files files, should be only one."); - - } elsif ($patched_files == 0) { - log_error($fname, NO_LINE_NUMBER, "Contains no patch."); - } - - checklines_trailing_empty_lines($lines); -} +#include PkgLint/Patches.pm sub checkfile_PLIST($) { my ($fname) = @_; |