File Coverage

blib/lib/Dpkg/Changelog/Entry/Debian.pm
Criterion Covered Total %
statement 121 142 85.2
branch 32 54 59.2
condition 6 12 50.0
subroutine 24 25 96.0
pod 15 15 100.0
total 198 248 79.8


line stmt bran cond sub pod time code
1             # Copyright © 2009 Raphaël Hertzog
2             # Copyright © 2012-2013 Guillem Jover
3             #
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.
8             #
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.
13             #
14             # You should have received a copy of the GNU General Public License
15             # along with this program. If not, see .
16              
17             package Dpkg::Changelog::Entry::Debian;
18              
19 2     2   15 use strict;
  2         4  
  2         57  
20 2     2   11 use warnings;
  2         3  
  2         106  
21              
22             our $VERSION = '2.00';
23             our @EXPORT_OK = qw(
24             match_header
25             match_trailer
26             find_closes
27             );
28              
29 2     2   12 use Exporter qw(import);
  2         3  
  2         49  
30 2     2   1187 use Time::Piece;
  2         20037  
  2         8  
31              
32 2     2   156 use Dpkg::Gettext;
  2         4  
  2         114  
33 2     2   13 use Dpkg::Control::Fields;
  2         4  
  2         180  
34 2     2   12 use Dpkg::Control::Changelog;
  2         4  
  2         82  
35 2     2   899 use Dpkg::Changelog::Entry;
  2         7  
  2         63  
36 2     2   14 use Dpkg::Version;
  2         6  
  2         181  
37              
38 2     2   13 use parent qw(Dpkg::Changelog::Entry);
  2         4  
  2         11  
39              
40             =encoding utf8
41              
42             =head1 NAME
43              
44             Dpkg::Changelog::Entry::Debian - represents a Debian changelog entry
45              
46             =head1 DESCRIPTION
47              
48             This class represents a Debian changelog entry.
49             It implements the generic interface Dpkg::Changelog::Entry.
50             Only functions specific to this implementation are described below,
51             the rest are inherited.
52              
53             =cut
54              
55             my $name_chars = qr/[-+0-9a-z.]/i;
56              
57             # The matched content is the source package name ($1), the version ($2),
58             # the target distributions ($3) and the options on the rest of the line ($4).
59             my $regex_header = qr{
60             ^
61             (\w$name_chars*) # Package name
62             \ \(([^\(\) \t]+)\) # Package version
63             ((?:\s+$name_chars+)+) # Target distribution
64             \; # Separator
65             (.*?) # Key=Value options
66             \s*$ # Trailing space
67             }xi;
68              
69             # The matched content is the maintainer name ($1), its email ($2),
70             # some blanks ($3) and the timestamp ($4), which is decomposed into
71             # day of week ($6), date-time ($7) and this into month name ($8).
72             my $regex_trailer = qr<
73             ^
74             \ \-\- # Trailer marker
75             \ (.*) # Maintainer name
76             \ \<(.*)\> # Maintainer email
77             (\ \ ?) # Blanks
78             (
79             ((\w+)\,\s*)? # Day of week (abbreviated)
80             (
81             \d{1,2}\s+ # Day of month
82             (\w+)\s+ # Month name (abbreviated)
83             \d{4}\s+ # Year
84             \d{1,2}:\d\d:\d\d\s+[-+]\d{4} # ISO 8601 date
85             )
86             )
87             \s*$ # Trailing space
88             >xo;
89              
90             my %week_day = map { $_ => 1 } qw(Mon Tue Wed Thu Fri Sat Sun);
91             my %month_abbrev = map { $_ => 1 } qw(
92             Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
93             );
94             my %month_name = map { $_ => } qw(
95             January February March April May June July
96             August September October November December
97             );
98              
99             =head1 METHODS
100              
101             =over 4
102              
103             =item @items = $entry->get_change_items()
104              
105             Return a list of change items. Each item contains at least one line.
106             A change line starting with an asterisk denotes the start of a new item.
107             Any change line like "C<[ Raphaël Hertzog ]>" is treated like an item of its
108             own even if it starts a set of items attributed to this person (the
109             following line necessarily starts a new item).
110              
111             =cut
112              
113             sub get_change_items {
114 2     2 1 7 my $self = shift;
115 2         5 my (@items, @blanks, $item);
116 2         6 foreach my $line (@{$self->get_part('changes')}) {
  2         8  
117 18 100       78 if ($line =~ /^\s*\*/) {
    100          
    100          
118 8 100       21 push @items, $item if defined $item;
119 8         21 $item = "$line\n";
120             } elsif ($line =~ /^\s*\[\s[^\]]+\s\]\s*$/) {
121 4 100       16 push @items, $item if defined $item;
122 4         14 push @items, "$line\n";
123 4         7 $item = undef;
124 4         10 @blanks = ();
125             } elsif ($line =~ /^\s*$/) {
126 2         5 push @blanks, "$line\n";
127             } else {
128 4 50       12 if (defined $item) {
129 4         13 $item .= "@blanks$line\n";
130             } else {
131 0         0 $item = "$line\n";
132             }
133 4         7 @blanks = ();
134             }
135             }
136 2 50       10 push @items, $item if defined $item;
137 2         11 return @items;
138             }
139              
140             =item @errors = $entry->parse_header()
141              
142             =item @errors = $entry->parse_trailer()
143              
144             Return a list of errors. Each item in the list is an error message
145             describing the problem. If the empty list is returned, no errors
146             have been found.
147              
148             =cut
149              
150             sub parse_header {
151 250     250 1 408 my $self = shift;
152 250         400 my @errors;
153 250 50 33     2460 if (defined($self->{header}) and $self->{header} =~ $regex_header) {
154 250         787 $self->{header_source} = $1;
155              
156 250         938 my $version = Dpkg::Version->new($2);
157 250         663 my ($ok, $msg) = version_check($version);
158 250 50       577 if ($ok) {
159 250         778 $self->{header_version} = $version;
160             } else {
161 0         0 push @errors, sprintf(g_("version '%s' is invalid: %s"), $version, $msg);
162             }
163              
164 250         378 @{$self->{header_dists}} = split ' ', $3;
  250         1289  
165              
166 250         677 my $options = $4;
167 250         876 $options =~ s/^\s+//;
168 250         946 my $f = Dpkg::Control::Changelog->new();
169 250         934 foreach my $opt (split(/\s*,\s*/, $options)) {
170 254 50       1487 unless ($opt =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i) {
171 0         0 push @errors, sprintf(g_("bad key-value after ';': '%s'"), $opt);
172 0         0 next;
173             }
174 254         772 my ($k, $v) = (field_capitalize($1), $2);
175 254 50       856 if (exists $f->{$k}) {
176 0         0 push @errors, sprintf(g_('repeated key-value %s'), $k);
177             } else {
178 254         545 $f->{$k} = $v;
179             }
180 254 100       877 if ($k eq 'Urgency') {
    50          
    50          
181 250 50       1335 push @errors, sprintf(g_('badly formatted urgency value: %s'), $v)
182             unless ($v =~ m/^([-0-9a-z]+)((\s+.*)?)$/i);
183             } elsif ($k eq 'Binary-Only') {
184 0 0       0 push @errors, sprintf(g_('bad binary-only value: %s'), $v)
185             unless ($v eq 'yes');
186             } elsif ($k =~ m/^X[BCS]+-/i) {
187             } else {
188 0         0 push @errors, sprintf(g_('unknown key-value %s'), $k);
189             }
190             }
191 250         671 $self->{header_fields} = $f;
192             } else {
193 0         0 push @errors, g_("the header doesn't match the expected regex");
194             }
195 250         790 return @errors;
196             }
197              
198             sub parse_trailer {
199 248     248 1 398 my $self = shift;
200 248         353 my @errors;
201 248 50 33     2325 if (defined($self->{trailer}) and $self->{trailer} =~ $regex_trailer) {
202 248         1297 $self->{trailer_maintainer} = "$1 <$2>";
203              
204 248 50       646 if ($3 ne ' ') {
205 0         0 push @errors, g_('badly formatted trailer line');
206             }
207              
208             # Validate the week day. Date::Parse used to ignore it, but Time::Piece
209             # is much more strict and it does not gracefully handle bogus values.
210 248 50 66     1051 if (defined $5 and not exists $week_day{$6}) {
211 0         0 push @errors, sprintf(g_('ignoring invalid week day \'%s\''), $6);
212             }
213              
214             # Ignore the week day ('%a, '), as we have validated it above.
215 248         1481 local $ENV{LC_ALL} = 'C';
216             eval {
217 248         925 my $tp = Time::Piece->strptime($7, '%d %b %Y %T %z');
218 248         17715 $self->{trailer_timepiece} = $tp;
219 248 50       472 } or do {
220             # Validate the month. Date::Parse used to accept both abbreviated
221             # and full months, but Time::Piece strptime() implementation only
222             # matches the abbreviated one with %b, which is what we want anyway.
223 0 0       0 if (not exists $month_abbrev{$8}) {
224             # We have to nest the conditionals because May is the same in
225             # full and abbreviated forms!
226 0 0       0 if (exists $month_name{$8}) {
227             push @errors, sprintf(g_('uses full \'%s\' instead of abbreviated month name \'%s\''),
228 0         0 $8, $month_name{$8});
229             } else {
230 0         0 push @errors, sprintf(g_('invalid abbreviated month name \'%s\''), $8);
231             }
232             }
233 0         0 push @errors, sprintf(g_("cannot parse non-conformant date '%s'"), $7);
234             };
235 248         8199 $self->{trailer_timestamp_date} = $4;
236             } else {
237 0         0 push @errors, g_("the trailer doesn't match the expected regex");
238             }
239 248         847 return @errors;
240             }
241              
242             =item $entry->normalize()
243              
244             Normalize the content. Strip whitespaces at end of lines, use a single
245             empty line to separate each part.
246              
247             =cut
248              
249             sub normalize {
250 0     0 1 0 my $self = shift;
251 0         0 $self->SUPER::normalize();
252             #XXX: recreate header/trailer
253             }
254              
255             =item $src = $entry->get_source()
256              
257             Return the name of the source package associated to the changelog entry.
258              
259             =cut
260              
261             sub get_source {
262 502     502 1 776 my $self = shift;
263              
264 502         1897 return $self->{header_source};
265             }
266              
267             =item $ver = $entry->get_version()
268              
269             Return the version associated to the changelog entry.
270              
271             =cut
272              
273             sub get_version {
274 1966     1966 1 23188 my $self = shift;
275              
276 1966         4524 return $self->{header_version};
277             }
278              
279             =item @dists = $entry->get_distributions()
280              
281             Return a list of target distributions for this version.
282              
283             =cut
284              
285             sub get_distributions {
286 504     504 1 771 my $self = shift;
287              
288 504 50       1128 if (defined $self->{header_dists}) {
289 504 100       984 return @{$self->{header_dists}} if wantarray;
  502         1905  
290 2         13 return $self->{header_dists}[0];
291             }
292 0         0 return;
293             }
294              
295             =item $fields = $entry->get_optional_fields()
296              
297             Return a set of optional fields exposed by the changelog entry.
298             It always returns a Dpkg::Control object (possibly empty though).
299              
300             =cut
301              
302             sub get_optional_fields {
303 1478     1478 1 2207 my $self = shift;
304 1478         2017 my $f;
305              
306 1478 50       3472 if (defined $self->{header_fields}) {
307 1478         2418 $f = $self->{header_fields};
308             } else {
309 0         0 $f = Dpkg::Control::Changelog->new();
310             }
311              
312 1478         2136 my @closes = find_closes(join("\n", @{$self->{changes}}));
  1478         7532  
313 1478 100       3362 if (@closes) {
314 1034         4067 $f->{Closes} = join(' ', @closes);
315             }
316              
317 1478         4069 return $f;
318             }
319              
320             =item $urgency = $entry->get_urgency()
321              
322             Return the urgency of the associated upload.
323              
324             =cut
325              
326             sub get_urgency {
327 738     738 1 1221 my $self = shift;
328 738         1386 my $f = $self->get_optional_fields();
329 738 50       1729 if (exists $f->{Urgency}) {
330 738         1423 $f->{Urgency} =~ s/\s.*$//;
331 738         2036 return lc($f->{Urgency});
332             }
333 0         0 return;
334             }
335              
336             =item $maint = $entry->get_maintainer()
337              
338             Return the string identifying the person who signed this changelog entry.
339              
340             =cut
341              
342             sub get_maintainer {
343 502     502 1 787 my $self = shift;
344              
345 502         1691 return $self->{trailer_maintainer};
346             }
347              
348             =item $time = $entry->get_timestamp()
349              
350             Return the timestamp of the changelog entry.
351              
352             =cut
353              
354             sub get_timestamp {
355 508     508 1 1496 my $self = shift;
356              
357 508         1641 return $self->{trailer_timestamp_date};
358             }
359              
360             =item $time = $entry->get_timepiece()
361              
362             Return the timestamp of the changelog entry as a Time::Piece object.
363              
364             This function might return undef if there was no timestamp.
365              
366             =cut
367              
368             sub get_timepiece {
369 1000     1000 1 14814 my $self = shift;
370              
371 1000         2906 return $self->{trailer_timepiece};
372             }
373              
374             =back
375              
376             =head1 UTILITY FUNCTIONS
377              
378             =over 4
379              
380             =item $bool = match_header($line)
381              
382             Checks if the line matches a valid changelog header line.
383              
384             =cut
385              
386             sub match_header {
387 3780     3780 1 5826 my $line = shift;
388              
389 3780         38143 return $line =~ /$regex_header/;
390             }
391              
392             =item $bool = match_trailer($line)
393              
394             Checks if the line matches a valid changelog trailing line.
395              
396             =cut
397              
398             sub match_trailer {
399 3524     3524 1 6386 my $line = shift;
400              
401 3524         19608 return $line =~ /$regex_trailer/;
402             }
403              
404             =item @closed_bugs = find_closes($changes)
405              
406             Takes one string as argument and finds "Closes: #123456, #654321" statements
407             as supported by the Debian Archive software in it. Returns all closed bug
408             numbers in an array.
409              
410             =cut
411              
412             sub find_closes {
413 1478     1478 1 2469 my $changes = shift;
414 1478         2074 my %closes;
415              
416 1478   66     10231 while ($changes && ($changes =~ m{
417             closes:\s*
418             (?:bug)?\#?\s?\d+
419             (?:,\s*(?:bug)?\#?\s?\d+)*
420             }pigx)) {
421 5822         55610 $closes{$_} = 1 foreach (${^MATCH} =~ /\#?\s?(\d+)/g);
422             }
423              
424 1478         5855 my @closes = sort { $a <=> $b } keys %closes;
  19779         27489  
425 1478         5552 return @closes;
426             }
427              
428             =back
429              
430             =head1 CHANGES
431              
432             =head2 Version 2.00 (dpkg 1.20.0)
433              
434             Remove methods: $entry->check_header(), $entry->check_trailer().
435              
436             Hide variables: $regex_header, $regex_trailer.
437              
438             =head2 Version 1.03 (dpkg 1.18.8)
439              
440             New methods: $entry->get_timepiece().
441              
442             =head2 Version 1.02 (dpkg 1.18.5)
443              
444             New methods: $entry->parse_header(), $entry->parse_trailer().
445              
446             Deprecated methods: $entry->check_header(), $entry->check_trailer().
447              
448             =head2 Version 1.01 (dpkg 1.17.2)
449              
450             New functions: match_header(), match_trailer()
451              
452             Deprecated variables: $regex_header, $regex_trailer
453              
454             =head2 Version 1.00 (dpkg 1.15.6)
455              
456             Mark the module as public.
457              
458             =cut
459              
460             1;