# # Dpkg::Changelog::Debian # # Copyright © 1996 Ian Jackson # Copyright © 2005 Frank Lichtenheld # # 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, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # =head1 NAME Dpkg::Changelog::Debian - parse Debian changelogs =head1 SYNOPSIS use Parse::DebianChangelog; my $chglog = Parse::DebianChangelog->init( { infile => 'debian/changelog', HTML => { outfile => 'changelog.html' } ); $chglog->html; # the following is semantically equivalent my $chglog = Parse::DebianChangelog->init(); $chglog->parse( { infile => 'debian/changelog' } ); $chglog->html( { outfile => 'changelog.html' } ); my $changes = $chglog->dpkg_str( { since => '1.0-1' } ); print $changes; =head1 DESCRIPTION Dpkg::Changelog::Debian parses Debian changelogs as described in the Debian policy (version 3.6.2.1 at the time of this writing). See section L<"SEE ALSO"> for locations where to find this definition. The parser tries to ignore most cruft like # or /* */ style comments, CVS comments, vim variables, emacs local variables and stuff from older changelogs with other formats at the end of the file. NOTE: most of these are ignored silently currently, there is no parser error issued for them. This should become configurable in the future. =head2 METHODS =cut package Dpkg::Changelog::Debian; use strict; use warnings; use Fcntl qw( :flock ); use English; use Date::Parse; use Dpkg; use Dpkg::Gettext; use Dpkg::Changelog qw( :util ); use base qw(Dpkg::Changelog); =pod =head3 parse Parses either the file named in configuration item C, the content of the filehandle in configuration item C, or the string saved in configuration item C (the latter requires IO::String). You can set a filename to use for reporting errors with configuration item C. Accepts a hash ref as optional argument which can contain configuration items. Returns C in case of error (e.g. "file not found", B parse errors) and the object if successful. If C was returned, you can get the reason for the failure by calling the L method. =cut sub parse { my ($self, $config) = @_; foreach my $c (keys %$config) { $self->{config}{$c} = $config->{$c}; } my ($fh, $file); if ($file = $self->{config}{infile}) { open $fh, '<', $file or do { $self->_do_fatal_error( _g("can't open file %s: %s"), $file, $! ); return undef; }; } elsif ($fh = $self->{config}{inhandle}) { $file = 'FileHandle'; } elsif (my $string = $self->{config}{instring}) { eval { require IO::String }; if ($@) { $self->_do_fatal_error( _g("can't load IO::String: %s"), $@ ); return undef; } $fh = IO::String->new( $string ); $file = 'String'; } else { $self->_do_fatal_error(_g('no changelog file specified')); return undef; } if (defined($self->{config}{reportfile})) { $file = $self->{config}{reportfile}; } $self->reset_parse_errors; $self->{data} = []; # based on /usr/lib/dpkg/parsechangelog/debian my $expect='first heading'; my $entry = new Dpkg::Changelog::Entry; my $blanklines = 0; my $unknowncounter = 1; # to make version unique, e.g. for using as id while (<$fh>) { s/\s*\n$//; # printf(STDERR "%-39.39s %-39.39s\n",$expect,$_); my $name_chars = qr/[-+0-9a-z.]/i; if (m/^(\w$name_chars*) \(([^\(\) \t]+)\)((\s+$name_chars+)+)\;/i) { unless ($expect eq 'first heading' || $expect eq 'next heading or eof') { $entry->{ERROR} = [ $file, $NR, sprintf(_g("found start of entry where expected %s"), $expect), "$_" ]; $self->_do_parse_error(@{$entry->{ERROR}}); } unless ($entry->is_empty) { $entry->{'Closes'} = find_closes( $entry->{Changes} ); # print STDERR, Dumper($entry); push @{$self->{data}}, $entry; $entry = new Dpkg::Changelog::Entry; last if $self->_abort_early; } { $entry->{'Source'} = "$1"; $entry->{'Version'} = "$2"; $entry->{'Header'} = "$_"; ($entry->{'Distribution'} = "$3") =~ s/^\s+//; $entry->{'Changes'} = $entry->{'Urgency_comment'} = ''; $entry->{'Urgency'} = $entry->{'Urgency_lc'} = 'unknown'; } (my $rhs = $POSTMATCH) =~ s/^\s+//; my %kvdone; # print STDERR "RHS: $rhs\n"; for my $kv (split(/\s*,\s*/,$rhs)) { $kv =~ m/^([-0-9a-z]+)\=\s*(.*\S)$/i || $self->_do_parse_error($file, $NR, sprintf(_g("bad key-value after \`;': \`%s'"), $kv)); my $k = ucfirst $1; my $v = $2; $kvdone{$k}++ && $self->_do_parse_error($file, $NR, sprintf(_g("repeated key-value %s"), $k)); if ($k eq 'Urgency') { $v =~ m/^([-0-9a-z]+)((\s+.*)?)$/i || $self->_do_parse_error($file, $NR, _g("badly formatted urgency value"), $v); $entry->{'Urgency'} = "$1"; $entry->{'Urgency_lc'} = lc("$1"); $entry->{'Urgency_comment'} = "$2"; } elsif ($k =~ m/^X[BCS]+-/i) { # Extensions - XB for putting in Binary, # XC for putting in Control, XS for putting in Source $entry->{$k}= $v; } else { $self->_do_parse_error($file, $NR, sprintf(_g("unknown key-value key %s - copying to XS-%s"), $k, $k)); $entry->{"XS-$k"} = $v; } } $expect= 'start of change data'; $blanklines = 0; } elsif (m/^(;;\s*)?Local variables:/io) { last; # skip Emacs variables at end of file } elsif (m/^vim:/io) { last; # skip vim variables at end of file } elsif (m/^\$\w+:.*\$/o) { next; # skip stuff that look like a CVS keyword } elsif (m/^\# /o) { next; # skip comments, even that's not supported } elsif (m,^/\*.*\*/,o) { next; # more comments } elsif (m/^(\w+\s+\w+\s+\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}\s+[\w\s]*\d{4})\s+(.*)\s+(<|\()(.*)(\)|>)/o || m/^(\w+\s+\w+\s+\d{1,2},?\s*\d{4})\s+(.*)\s+(<|\()(.*)(\)|>)/o || m/^(\w[-+0-9a-z.]*) \(([^\(\) \t]+)\)\;?/io || m/^([\w.+-]+)(-| )(\S+) Debian (\S+)/io || m/^Changes from version (.*) to (.*):/io || m/^Changes for [\w.+-]+-[\w.+-]+:?$/io || m/^Old Changelog:$/io || m/^(?:\d+:)?\w[\w.+~-]*:?$/o) { # save entries on old changelog format verbatim # we assume the rest of the file will be in old format once we # hit it for the first time $self->{oldformat} = "$_\n"; $self->{oldformat} .= join "", <$fh>; } elsif (m/^\S/) { $self->_do_parse_error($file, $NR, _g("badly formatted heading line"), "$_"); } elsif (m/^ \-\- (.*) <(.*)>( ?)((\w+\,\s*)?\d{1,2}\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d\s+[-+]\d{4}(\s+\([^\\\(\)]\))?)$/o) { $expect eq 'more change data or trailer' || $self->_do_parse_error($file, $NR, sprintf(_g("found trailer where expected %s"), $expect), "$_"); if ($3 ne ' ') { $self->_do_parse_error($file, $NR, _g( "badly formatted trailer line" ), "$_"); } $entry->{'Trailer'} = $_; $entry->{'Maintainer'} = "$1 <$2>" unless $entry->{'Maintainer'}; unless($entry->{'Date'} && defined $entry->{'Timestamp'}) { $entry->{'Date'} = "$4"; $entry->{'Timestamp'} = str2time($4); unless (defined $entry->{'Timestamp'}) { $self->_do_parse_error( $file, $NR, sprintf(_g("couldn't parse date %s"), "$4")); } } $expect = 'next heading or eof'; } elsif (m/^ \-\-/) { $entry->{ERROR} = [ $file, $NR, _g( "badly formatted trailer line" ), "$_" ]; $self->_do_parse_error(@{$entry->{ERROR}}); # $expect = 'next heading or eof' # if $expect eq 'more change data or trailer'; } elsif (m/^\s{2,}(\S)/) { $expect eq 'start of change data' || $expect eq 'more change data or trailer' || do { $self->_do_parse_error($file, $NR, sprintf(_g("found change data where expected %s"), $expect), "$_"); if (($expect eq 'next heading or eof') && !$entry->is_empty) { # lets assume we have missed the actual header line $entry->{'Closes'} = find_closes( $entry->{Changes} ); # print STDERR, Dumper($entry); push @{$self->{data}}, $entry; $entry = new Dpkg::Changelog::Entry; $entry->{Source} = $entry->{Distribution} = $entry->{Urgency} = $entry->{Urgency_LC} = 'unknown'; $entry->{Version} = 'unknown'.($unknowncounter++); $entry->{Urgency_Comment} = ''; $entry->{ERROR} = [ $file, $NR, sprintf(_g("found change data where expected %s"), $expect), "$_" ]; } }; $entry->{'Changes'} .= (" \n" x $blanklines)." $_\n"; if (!$entry->{'Items'} || ($1 eq '*')) { $entry->{'Items'} ||= []; push @{$entry->{'Items'}}, "$_\n"; } else { $entry->{'Items'}[-1] .= (" \n" x $blanklines)." $_\n"; } $blanklines = 0; $expect = 'more change data or trailer'; } elsif (!m/\S/) { next if $expect eq 'start of change data' || $expect eq 'next heading or eof'; $expect eq 'more change data or trailer' || $self->_do_parse_error($file, $NR, sprintf(_g("found blank line where expected %s"), $expect)); $blanklines++; } else { $self->_do_parse_error($file, $NR, _g( "unrecognised line" ), "$_"); ($expect eq 'start of change data' || $expect eq 'more change data or trailer') && do { # lets assume change data if we expected it $entry->{'Changes'} .= (" \n" x $blanklines)." $_\n"; if (!$entry->{'Items'}) { $entry->{'Items'} ||= []; push @{$entry->{'Items'}}, "$_\n"; } else { $entry->{'Items'}[-1] .= (" \n" x $blanklines)." $_\n"; } $blanklines = 0; $expect = 'more change data or trailer'; $entry->{ERROR} = [ $file, $NR, _g( "unrecognised line" ), "$_" ]; }; } } $expect eq 'next heading or eof' || do { $entry->{ERROR} = [ $file, $NR, sprintf(_g("found eof where expected %s"), $expect) ]; $self->_do_parse_error( @{$entry->{ERROR}} ); }; unless ($entry->is_empty) { $entry->{'Closes'} = find_closes( $entry->{Changes} ); push @{$self->{data}}, $entry; } if ($self->{config}{infile}) { close $fh or do { $self->_do_fatal_error( _g("can't close file %s: %s"), $file, $!); return undef; }; } # use Data::Dumper; # print Dumper( $self ); return $self; } 1; __END__ =head1 SEE ALSO Dpkg::Changelog Description of the Debian changelog format in the Debian policy: L. =head1 AUTHOR Frank Lichtenheld, Efrank@lichtenheld.deE =head1 COPYRIGHT AND LICENSE Copyright (C) 2005 by Frank Lichtenheld 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, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =cut