# -*- perl -*-
#
# Copyright © 1998 Christian Schwarz and Richard Braakman
# Copyright © 2009 Russ Allbery
# Copyright © 2020 Felix Lechner
#
# 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 .
package Lintian::Data::Policy::Releases;
use v5.20;
use warnings;
use utf8;
use Carp qw(croak);
use Const::Fast;
use Date::Parse qw(str2time);
use List::SomeUtils qw(first_value);
use IPC::Run3;
use HTTP::Tiny;
use JSON::MaybeXS;
use List::SomeUtils qw(minmax);
use List::UtilsBy qw(rev_nsort_by);
use Path::Tiny;
use Time::Moment;
use Time::Piece;
use Unicode::UTF8 qw(decode_utf8 encode_utf8);
use Moo;
use namespace::clean;
const my $SLASH => q{/};
const my $WAIT_STATUS_SHIFT => 8;
=head1 NAME
Lintian::Data::Policy::Releases - Lintian interface for policy releases
=head1 SYNOPSIS
use Lintian::Data::Policy::Releases;
=head1 DESCRIPTION
This module provides a way to load data files for policy releases.
=head1 INSTANCE METHODS
=over 4
=item title
=item location
=item preamble
=item ordered_versions
=item by_version
=item max_dots
=cut
has title => (
is => 'rw',
default => 'Debian Policy Releases'
);
has location => (
is => 'rw',
default => 'debian-policy/releases.json'
);
has preamble => (is => 'rw');
has ordered_versions => (is => 'rw', default => sub { [] });
has by_version => (is => 'rw', default => sub { {} });
has max_dots => (is => 'rw');
=item latest_version
=cut
sub latest_version {
my ($self) = @_;
return $self->ordered_versions->[0];
}
=item normalize
=cut
sub normalize {
my ($self, $version) = @_;
my $have = $version =~ tr{\.}{};
my $need = $self->max_dots - $have;
$version .= '.0' for (1..$need);
return $version;
}
=item is_known
=cut
sub is_known {
my ($self, $version) = @_;
my $normalized = $self->normalize($version);
return exists $self->by_version->{$normalized};
}
=item epoch
=cut
sub epoch {
my ($self, $version) = @_;
my $normalized = $self->normalize($version);
my $release = $self->by_version->{$normalized};
return undef
unless defined $release;
return $release->{epoch};
}
=item author
=cut
sub author {
my ($self, $version) = @_;
my $normalized = $self->normalize($version);
my $release = $self->by_version->{$normalized};
return undef
unless defined $release;
return $release->{author};
}
=item load
=cut
sub load {
my ($self, $search_space, $our_vendor) = @_;
my @candidates = map { $_ . $SLASH . $self->location } @{$search_space};
my $path = first_value { -e } @candidates;
croak encode_utf8('Unknown data file: ' . $self->location)
unless length $path;
my $json = path($path)->slurp;
my $data = decode_json($json);
$self->preamble($data->{preamble});
my @sorted = rev_nsort_by { $_->{epoch} } @{$data->{releases}};
my @ordered_versions = map { $_->{version} } @sorted;
$self->ordered_versions(\@ordered_versions);
my @dot_count = map { tr{\.}{} } @ordered_versions;
my (undef, $max_dots) = minmax @dot_count;
$self->max_dots($max_dots);
# normalize versions
$_->{version} = $self->normalize($_->{version})for @{$data->{releases}};
my %by_version;
$by_version{$_->{version}} = $_ for @{$data->{releases}};
$self->by_version(\%by_version);
return;
}
=item refresh
=cut
sub refresh {
my ($self, $archive, $basedir) = @_;
my $changelog_url
= 'https://salsa.debian.org/dbnpolicy/policy/-/raw/master/debian/changelog?inline=false';
my $response = HTTP::Tiny->new->get($changelog_url);
die encode_utf8("Failed to get $changelog_url!\n")
unless $response->{success};
my $tempfile_tiny = Path::Tiny->tempfile;
$tempfile_tiny->spew($response->{content});
my @command = (
qw{dpkg-parsechangelog --format rfc822 --all --file},
$tempfile_tiny->stringify
);
my $rfc822;
my $stderr;
run3(\@command, \undef, \$rfc822, \$stderr);
my $status = ($? >> $WAIT_STATUS_SHIFT);
# already in UTF-8
die $stderr
if $status;
my $deb822 = Lintian::Deb822::File->new;
my @sections = $deb822->parse_string(decode_utf8($rfc822));
my @releases;
for my $section (@sections) {
my $epoch = str2time($section->value('Date'), 'GMT');
my $moment = Time::Moment->from_epoch($epoch);
my $timestamp = $moment->strftime('%Y-%m-%dT%H:%M:%S%Z');
my @closes = sort { $a <=> $b } $section->trimmed_list('Closes');
my @changes = split(/\n/, $section->text('Changes'));
my %release;
$release{version} = $section->value('Version');
$release{timestamp} = $timestamp;
$release{epoch} = $epoch;
$release{closes} = \@closes;
$release{changes} = \@changes;
$release{author} = $section->value('Maintainer');
push(@releases, \%release);
}
my @sorted = rev_nsort_by { $_->{epoch} } @releases;
my %preamble;
$preamble{title} = $self->title;
$preamble{'last-update'} = gmtime->datetime . 'Z';
my %all;
$all{preamble} = \%preamble;
$all{releases} = \@sorted;
# convert to UTF-8 prior to encoding in JSON
my $encoder = JSON->new;
$encoder->canonical;
$encoder->utf8;
$encoder->pretty;
my $json = $encoder->encode(\%all);
my $datapath = "$basedir/" . $self->location;
my $parentdir = path($datapath)->parent->stringify;
path($parentdir)->mkpath
unless -e $parentdir;
# already in UTF-8
path($datapath)->spew($json);
return 1;
}
=back
=head1 AUTHOR
Originally written by Felix Lechner for Lintian.
=head1 SEE ALSO
lintian(1)
=cut
1;
# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et