123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org>
- # Copyright © 2012-2013 Guillem Jover <guillem@debian.org>
- #
- # 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, see <https://www.gnu.org/licenses/>.
- package Dpkg::Changelog::Entry::Debian;
- use strict;
- use warnings;
- our $VERSION = '1.03';
- our @EXPORT_OK = qw(
- $regex_header
- $regex_trailer
- match_header
- match_trailer
- find_closes
- );
- use Exporter qw(import);
- use Time::Piece;
- use Dpkg::Gettext;
- use Dpkg::Control::Fields;
- use Dpkg::Control::Changelog;
- use Dpkg::Changelog::Entry;
- use Dpkg::Version;
- use parent qw(Dpkg::Changelog::Entry);
- =encoding utf8
- =head1 NAME
- Dpkg::Changelog::Entry::Debian - represents a Debian changelog entry
- =head1 DESCRIPTION
- This object represents a Debian changelog entry. It implements the
- generic interface Dpkg::Changelog::Entry. Only functions specific to this
- implementation are described below.
- =cut
- my $name_chars = qr/[-+0-9a-z.]/i;
- # XXX: Backwards compatibility, stop exporting on VERSION 2.00.
- ## no critic (Variables::ProhibitPackageVars)
- # The matched content is the source package name ($1), the version ($2),
- # the target distributions ($3) and the options on the rest of the line ($4).
- our $regex_header = qr{
- ^
- (\w$name_chars*) # Package name
- \ \(([^\(\) \t]+)\) # Package version
- ((?:\s+$name_chars+)+) # Target distribution
- \; # Separator
- (.*?) # Key=Value options
- \s*$ # Trailing space
- }xi;
- # The matched content is the maintainer name ($1), its email ($2),
- # some blanks ($3) and the timestamp ($4), which is decomposed into
- # day of week ($6), date-time ($7) and this into month name ($8).
- our $regex_trailer = qr<
- ^
- \ \-\- # Trailer marker
- \ (.*) # Maintainer name
- \ \<(.*)\> # Maintainer email
- (\ \ ?) # Blanks
- (
- ((\w+)\,\s*)? # Day of week (abbreviated)
- (
- \d{1,2}\s+ # Day of month
- (\w+)\s+ # Month name (abbreviated)
- \d{4}\s+ # Year
- \d{1,2}:\d\d:\d\d\s+[-+]\d{4} # ISO 8601 date
- )
- )
- \s*$ # Trailing space
- >xo;
- my %week_day = map { $_ => 1 } qw(Mon Tue Wed Thu Fri Sat Sun);
- my %month_abbrev = map { $_ => 1 } qw(
- Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
- );
- my %month_name = map { $_ => } qw(
- January February March April May June July
- August September October November December
- );
- ## use critic
- =head1 METHODS
- =over 4
- =item @items = $entry->get_change_items()
- Return a list of change items. Each item contains at least one line.
- A change line starting with an asterisk denotes the start of a new item.
- Any change line like "C<[ Raphaël Hertzog ]>" is treated like an item of its
- own even if it starts a set of items attributed to this person (the
- following line necessarily starts a new item).
- =cut
- sub get_change_items {
- my $self = shift;
- my (@items, @blanks, $item);
- foreach my $line (@{$self->get_part('changes')}) {
- if ($line =~ /^\s*\*/) {
- push @items, $item if defined $item;
- $item = "$line\n";
- } elsif ($line =~ /^\s*\[\s[^\]]+\s\]\s*$/) {
- push @items, $item if defined $item;
- push @items, "$line\n";
- $item = undef;
- @blanks = ();
- } elsif ($line =~ /^\s*$/) {
- push @blanks, "$line\n";
- } else {
- if (defined $item) {
- $item .= "@blanks$line\n";
- } else {
- $item = "$line\n";
- }
- @blanks = ();
- }
- }
- push @items, $item if defined $item;
- return @items;
- }
- =item @errors = $entry->parse_header()
- =item @errors = $entry->parse_trailer()
- Return a list of errors. Each item in the list is an error message
- describing the problem. If the empty list is returned, no errors
- have been found.
- =cut
- sub parse_header {
- my $self = shift;
- my @errors;
- if (defined($self->{header}) and $self->{header} =~ $regex_header) {
- $self->{header_source} = $1;
- my $version = Dpkg::Version->new($2);
- my ($ok, $msg) = version_check($version);
- if ($ok) {
- $self->{header_version} = $version;
- } else {
- push @errors, sprintf(g_("version '%s' is invalid: %s"), $version, $msg);
- }
- @{$self->{header_dists}} = split ' ', $3;
- my $options = $4;
- $options =~ s/^\s+//;
- my $f = Dpkg::Control::Changelog->new();
- foreach my $opt (split(/\s*,\s*/, $options)) {
- unless ($opt =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i) {
- push @errors, sprintf(g_("bad key-value after ';': '%s'"), $opt);
- next;
- }
- my ($k, $v) = (field_capitalize($1), $2);
- if (exists $f->{$k}) {
- push @errors, sprintf(g_('repeated key-value %s'), $k);
- } else {
- $f->{$k} = $v;
- }
- if ($k eq 'Urgency') {
- push @errors, sprintf(g_('badly formatted urgency value: %s'), $v)
- unless ($v =~ m/^([-0-9a-z]+)((\s+.*)?)$/i);
- } elsif ($k eq 'Binary-Only') {
- push @errors, sprintf(g_('bad binary-only value: %s'), $v)
- unless ($v eq 'yes');
- } elsif ($k =~ m/^X[BCS]+-/i) {
- } else {
- push @errors, sprintf(g_('unknown key-value %s'), $k);
- }
- }
- $self->{header_fields} = $f;
- } else {
- push @errors, g_("the header doesn't match the expected regex");
- }
- return @errors;
- }
- sub parse_trailer {
- my $self = shift;
- my @errors;
- if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
- $self->{trailer_maintainer} = "$1 <$2>";
- if ($3 ne ' ') {
- push @errors, g_('badly formatted trailer line');
- }
- # Validate the week day. Date::Parse used to ignore it, but Time::Piece
- # is much more strict and it does not gracefully handle bogus values.
- if (defined $5 and not exists $week_day{$6}) {
- push @errors, sprintf(g_('ignoring invalid week day \'%s\''), $6);
- }
- # Ignore the week day ('%a, '), as we have validated it above.
- local $ENV{LC_ALL} = 'C';
- eval {
- my $tp = Time::Piece->strptime($7, '%d %b %Y %T %z');
- $self->{trailer_timepiece} = $tp;
- } or do {
- # Validate the month. Date::Parse used to accept both abbreviated
- # and full months, but Time::Piece strptime() implementation only
- # matches the abbreviated one with %b, which is what we want anyway.
- if (not exists $month_abbrev{$8}) {
- # We have to nest the conditionals because May is the same in
- # full and abbreviated forms!
- if (exists $month_name{$8}) {
- push @errors, sprintf(g_('uses full instead of abbreviated month name \'%s\''),
- $8, $month_name{$8});
- } else {
- push @errors, sprintf(g_('invalid abbreviated month name \'%s\''), $8);
- }
- }
- push @errors, sprintf(g_("cannot parse non-comformant date '%s'"), $7);
- };
- $self->{trailer_timestamp_date} = $4;
- } else {
- push @errors, g_("the trailer doesn't match the expected regex");
- }
- return @errors;
- }
- =item $entry->check_header()
- Obsolete method. Use parse_header() instead.
- =cut
- sub check_header {
- my $self = shift;
- warnings::warnif('deprecated',
- 'obsolete check_header(), use parse_header() instead');
- return $self->parse_header();
- }
- =item $entry->check_trailer()
- Obsolete method. Use parse_trailer() instead.
- =cut
- sub check_trailer {
- my $self = shift;
- warnings::warnif('deprecated',
- 'obsolete check_trailer(), use parse_trailer() instead');
- return $self->parse_header();
- }
- =item $entry->normalize()
- Normalize the content. Strip whitespaces at end of lines, use a single
- empty line to separate each part.
- =cut
- sub normalize {
- my $self = shift;
- $self->SUPER::normalize();
- #XXX: recreate header/trailer
- }
- =item $src = $entry->get_source()
- Return the name of the source package associated to the changelog entry.
- =cut
- sub get_source {
- my $self = shift;
- return $self->{header_source};
- }
- =item $ver = $entry->get_version()
- Return the version associated to the changelog entry.
- =cut
- sub get_version {
- my $self = shift;
- return $self->{header_version};
- }
- =item @dists = $entry->get_distributions()
- Return a list of target distributions for this version.
- =cut
- sub get_distributions {
- my $self = shift;
- if (defined $self->{header_dists}) {
- return @{$self->{header_dists}} if wantarray;
- return $self->{header_dists}[0];
- }
- return;
- }
- =item $fields = $entry->get_optional_fields()
- Return a set of optional fields exposed by the changelog entry.
- It always returns a Dpkg::Control object (possibly empty though).
- =cut
- sub get_optional_fields {
- my $self = shift;
- my $f;
- if (defined $self->{header_fields}) {
- $f = $self->{header_fields};
- } else {
- $f = Dpkg::Control::Changelog->new();
- }
- my @closes = find_closes(join("\n", @{$self->{changes}}));
- if (@closes) {
- $f->{Closes} = join(' ', @closes);
- }
- return $f;
- }
- =item $urgency = $entry->get_urgency()
- Return the urgency of the associated upload.
- =cut
- sub get_urgency {
- my $self = shift;
- my $f = $self->get_optional_fields();
- if (exists $f->{Urgency}) {
- $f->{Urgency} =~ s/\s.*$//;
- return lc($f->{Urgency});
- }
- return;
- }
- =item $maint = $entry->get_maintainer()
- Return the string identifying the person who signed this changelog entry.
- =cut
- sub get_maintainer {
- my $self = shift;
- return $self->{trailer_maintainer};
- }
- =item $time = $entry->get_timestamp()
- Return the timestamp of the changelog entry.
- =cut
- sub get_timestamp {
- my $self = shift;
- return $self->{trailer_timestamp_date};
- }
- =item $time = $entry->get_timepiece()
- Return the timestamp of the changelog entry as a Time::Piece object.
- This function might return undef if there was no timestamp.
- =cut
- sub get_timepiece {
- my $self = shift;
- return $self->{trailer_timepiece};
- }
- =back
- =head1 UTILITY FUNCTIONS
- =over 4
- =item $bool = match_header($line)
- Checks if the line matches a valid changelog header line.
- =cut
- sub match_header {
- my $line = shift;
- return $line =~ /$regex_header/;
- }
- =item $bool = match_trailer($line)
- Checks if the line matches a valid changelog trailing line.
- =cut
- sub match_trailer {
- my $line = shift;
- return $line =~ /$regex_trailer/;
- }
- =item @closed_bugs = find_closes($changes)
- Takes one string as argument and finds "Closes: #123456, #654321" statements
- as supported by the Debian Archive software in it. Returns all closed bug
- numbers in an array.
- =cut
- sub find_closes {
- my $changes = shift;
- my %closes;
- while ($changes && ($changes =~ m{
- closes:\s*
- (?:bug)?\#?\s?\d+
- (?:,\s*(?:bug)?\#?\s?\d+)*
- }pigx)) {
- $closes{$_} = 1 foreach (${^MATCH} =~ /\#?\s?(\d+)/g);
- }
- my @closes = sort { $a <=> $b } keys %closes;
- return @closes;
- }
- =back
- =head1 CHANGES
- =head2 Version 1.03 (dpkg 1.18.8)
- New methods: $entry->get_timepiece().
- =head2 Version 1.02 (dpkg 1.18.5)
- New methods: $entry->parse_header(), $entry->parse_trailer().
- Deprecated methods: $entry->check_header(), $entry->check_trailer().
- =head2 Version 1.01 (dpkg 1.17.2)
- New functions: match_header(), match_trailer()
- Deprecated variables: $regex_header, $regex_trailer
- =head2 Version 1.00 (dpkg 1.15.6)
- Mark the module as public.
- =cut
- 1;
|