1 # Copyright © 2009 Raphaël Hertzog <hertzog@debian.org>
2 # Copyright © 2012-2013 Guillem Jover <guillem@debian.org>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 package Dpkg::Changelog::Entry::Debian;
22 our $VERSION = '1.03';
31 use Exporter qw(import);
35 use Dpkg::Control::Fields;
36 use Dpkg::Control::Changelog;
37 use Dpkg::Changelog::Entry;
40 use parent qw(Dpkg::Changelog::Entry);
46 Dpkg::Changelog::Entry::Debian - represents a Debian changelog entry
50 This object represents a Debian changelog entry. It implements the
51 generic interface Dpkg::Changelog::Entry. Only functions specific to this
52 implementation are described below.
56 my $name_chars = qr/[-+0-9a-z.]/i;
58 # XXX: Backwards compatibility, stop exporting on VERSION 2.00.
59 ## no critic (Variables::ProhibitPackageVars)
61 # The matched content is the source package name ($1), the version ($2),
62 # the target distributions ($3) and the options on the rest of the line ($4).
63 our $regex_header = qr{
65 (\w$name_chars*) # Package name
66 \ \(([^\(\) \t]+)\) # Package version
67 ((?:\s+$name_chars+)+) # Target distribution
69 (.*?) # Key=Value options
73 # The matched content is the maintainer name ($1), its email ($2),
74 # some blanks ($3) and the timestamp ($4), which is decomposed into
75 # day of week ($6), date-time ($7) and this into month name ($8).
76 our $regex_trailer = qr<
78 \ \-\- # Trailer marker
79 \ (.*) # Maintainer name
80 \ \<(.*)\> # Maintainer email
83 ((\w+)\,\s*)? # Day of week (abbreviated)
85 \d{1,2}\s+ # Day of month
86 (\w+)\s+ # Month name (abbreviated)
88 \d{1,2}:\d\d:\d\d\s+[-+]\d{4} # ISO 8601 date
94 my %week_day = map { $_ => 1 } qw(Mon Tue Wed Thu Fri Sat Sun);
95 my %month_abbrev = map { $_ => 1 } qw(
96 Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
98 my %month_name = map { $_ => } qw(
99 January February March April May June July
100 August September October November December
109 =item @items = $entry->get_change_items()
111 Return a list of change items. Each item contains at least one line.
112 A change line starting with an asterisk denotes the start of a new item.
113 Any change line like "C<[ Raphaël Hertzog ]>" is treated like an item of its
114 own even if it starts a set of items attributed to this person (the
115 following line necessarily starts a new item).
119 sub get_change_items {
121 my (@items, @blanks, $item);
122 foreach my $line (@{$self->get_part('changes')}) {
123 if ($line =~ /^\s*\*/) {
124 push @items, $item if defined $item;
126 } elsif ($line =~ /^\s*\[\s[^\]]+\s\]\s*$/) {
127 push @items, $item if defined $item;
128 push @items, "$line\n";
131 } elsif ($line =~ /^\s*$/) {
132 push @blanks, "$line\n";
135 $item .= "@blanks$line\n";
142 push @items, $item if defined $item;
146 =item @errors = $entry->parse_header()
148 =item @errors = $entry->parse_trailer()
150 Return a list of errors. Each item in the list is an error message
151 describing the problem. If the empty list is returned, no errors
159 if (defined($self->{header}) and $self->{header} =~ $regex_header) {
160 $self->{header_source} = $1;
162 my $version = Dpkg::Version->new($2);
163 my ($ok, $msg) = version_check($version);
165 $self->{header_version} = $version;
167 push @errors, sprintf(g_("version '%s' is invalid: %s"), $version, $msg);
170 @{$self->{header_dists}} = split ' ', $3;
173 $options =~ s/^\s+//;
174 my $f = Dpkg::Control::Changelog->new();
175 foreach my $opt (split(/\s*,\s*/, $options)) {
176 unless ($opt =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i) {
177 push @errors, sprintf(g_("bad key-value after ';': '%s'"), $opt);
180 my ($k, $v) = (field_capitalize($1), $2);
181 if (exists $f->{$k}) {
182 push @errors, sprintf(g_('repeated key-value %s'), $k);
186 if ($k eq 'Urgency') {
187 push @errors, sprintf(g_('badly formatted urgency value: %s'), $v)
188 unless ($v =~ m/^([-0-9a-z]+)((\s+.*)?)$/i);
189 } elsif ($k eq 'Binary-Only') {
190 push @errors, sprintf(g_('bad binary-only value: %s'), $v)
191 unless ($v eq 'yes');
192 } elsif ($k =~ m/^X[BCS]+-/i) {
194 push @errors, sprintf(g_('unknown key-value %s'), $k);
197 $self->{header_fields} = $f;
199 push @errors, g_("the header doesn't match the expected regex");
207 if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
208 $self->{trailer_maintainer} = "$1 <$2>";
211 push @errors, g_('badly formatted trailer line');
214 # Validate the week day. Date::Parse used to ignore it, but Time::Piece
215 # is much more strict and it does not gracefully handle bogus values.
216 if (defined $5 and not exists $week_day{$6}) {
217 push @errors, sprintf(g_('ignoring invalid week day \'%s\''), $6);
220 # Ignore the week day ('%a, '), as we have validated it above.
221 local $ENV{LC_ALL} = 'C';
223 my $tp = Time::Piece->strptime($7, '%d %b %Y %T %z');
224 $self->{trailer_timepiece} = $tp;
226 # Validate the month. Date::Parse used to accept both abbreviated
227 # and full months, but Time::Piece strptime() implementation only
228 # matches the abbreviated one with %b, which is what we want anyway.
229 if (not exists $month_abbrev{$8}) {
230 # We have to nest the conditionals because May is the same in
231 # full and abbreviated forms!
232 if (exists $month_name{$8}) {
233 push @errors, sprintf(g_('uses full instead of abbreviated month name \'%s\''),
234 $8, $month_name{$8});
236 push @errors, sprintf(g_('invalid abbreviated month name \'%s\''), $8);
239 push @errors, sprintf(g_("cannot parse non-comformant date '%s'"), $7);
241 $self->{trailer_timestamp_date} = $4;
243 push @errors, g_("the trailer doesn't match the expected regex");
248 =item $entry->check_header()
250 Obsolete method. Use parse_header() instead.
257 warnings::warnif('deprecated',
258 'obsolete check_header(), use parse_header() instead');
260 return $self->parse_header();
263 =item $entry->check_trailer()
265 Obsolete method. Use parse_trailer() instead.
272 warnings::warnif('deprecated',
273 'obsolete check_trailer(), use parse_trailer() instead');
275 return $self->parse_header();
278 =item $entry->normalize()
280 Normalize the content. Strip whitespaces at end of lines, use a single
281 empty line to separate each part.
287 $self->SUPER::normalize();
288 #XXX: recreate header/trailer
291 =item $src = $entry->get_source()
293 Return the name of the source package associated to the changelog entry.
300 return $self->{header_source};
303 =item $ver = $entry->get_version()
305 Return the version associated to the changelog entry.
312 return $self->{header_version};
315 =item @dists = $entry->get_distributions()
317 Return a list of target distributions for this version.
321 sub get_distributions {
324 if (defined $self->{header_dists}) {
325 return @{$self->{header_dists}} if wantarray;
326 return $self->{header_dists}[0];
331 =item $fields = $entry->get_optional_fields()
333 Return a set of optional fields exposed by the changelog entry.
334 It always returns a Dpkg::Control object (possibly empty though).
338 sub get_optional_fields {
342 if (defined $self->{header_fields}) {
343 $f = $self->{header_fields};
345 $f = Dpkg::Control::Changelog->new();
348 my @closes = find_closes(join("\n", @{$self->{changes}}));
350 $f->{Closes} = join(' ', @closes);
356 =item $urgency = $entry->get_urgency()
358 Return the urgency of the associated upload.
364 my $f = $self->get_optional_fields();
365 if (exists $f->{Urgency}) {
366 $f->{Urgency} =~ s/\s.*$//;
367 return lc($f->{Urgency});
372 =item $maint = $entry->get_maintainer()
374 Return the string identifying the person who signed this changelog entry.
381 return $self->{trailer_maintainer};
384 =item $time = $entry->get_timestamp()
386 Return the timestamp of the changelog entry.
393 return $self->{trailer_timestamp_date};
396 =item $time = $entry->get_timepiece()
398 Return the timestamp of the changelog entry as a Time::Piece object.
400 This function might return undef if there was no timestamp.
407 return $self->{trailer_timepiece};
412 =head1 UTILITY FUNCTIONS
416 =item $bool = match_header($line)
418 Checks if the line matches a valid changelog header line.
425 return $line =~ /$regex_header/;
428 =item $bool = match_trailer($line)
430 Checks if the line matches a valid changelog trailing line.
437 return $line =~ /$regex_trailer/;
440 =item @closed_bugs = find_closes($changes)
442 Takes one string as argument and finds "Closes: #123456, #654321" statements
443 as supported by the Debian Archive software in it. Returns all closed bug
452 while ($changes && ($changes =~ m{
455 (?:,\s*(?:bug)?\#?\s?\d+)*
457 $closes{$_} = 1 foreach (${^MATCH} =~ /\#?\s?(\d+)/g);
460 my @closes = sort { $a <=> $b } keys %closes;
468 =head2 Version 1.03 (dpkg 1.18.8)
470 New methods: $entry->get_timepiece().
472 =head2 Version 1.02 (dpkg 1.18.5)
474 New methods: $entry->parse_header(), $entry->parse_trailer().
476 Deprecated methods: $entry->check_header(), $entry->check_trailer().
478 =head2 Version 1.01 (dpkg 1.17.2)
480 New functions: match_header(), match_trailer()
482 Deprecated variables: $regex_header, $regex_trailer
484 =head2 Version 1.00 (dpkg 1.15.6)
486 Mark the module as public.