summaryrefslogtreecommitdiffstats
path: root/lib/Lintian/Relation
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/Lintian/Relation.pm788
-rw-r--r--lib/Lintian/Relation/Predicate.pm553
-rw-r--r--lib/Lintian/Relation/Version.pm213
3 files changed, 1554 insertions, 0 deletions
diff --git a/lib/Lintian/Relation.pm b/lib/Lintian/Relation.pm
new file mode 100644
index 0000000..b7b4b67
--- /dev/null
+++ b/lib/Lintian/Relation.pm
@@ -0,0 +1,788 @@
+# -*- perl -*-
+# Lintian::Relation -- operations on dependencies and relationships
+
+# Copyright (C) 1998 Christian Schwarz and Richard Braakman
+# Copyright (C) 2004-2009 Russ Allbery <rra@debian.org>
+# Copyright (C) 2018 Chris Lamb <lamby@debian.org>
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+package Lintian::Relation;
+
+use v5.20;
+use warnings;
+use utf8;
+
+use Carp qw(confess);
+use Const::Fast;
+use List::SomeUtils qw(any);
+use Unicode::UTF8 qw(encode_utf8);
+
+use Lintian::Relation::Predicate;
+
+use Moo;
+use namespace::clean;
+
+use constant {
+ VISIT_PRED_NAME => 0,
+ VISIT_PRED_FULL => 1,
+ VISIT_OR_CLAUSE_FULL => 3,
+ VISIT_STOP_FIRST_MATCH => 4,
+};
+
+const my $EMPTY => q{};
+
+const my $BRANCH_TYPE => 0;
+const my $PREDICATE => 1;
+
+const my $FALSE => 0;
+
+=head1 NAME
+
+Lintian::Relation - Lintian operations on dependencies and relationships
+
+=head1 SYNOPSIS
+
+ my $depends = Lintian::Relation->new('foo | bar, baz');
+ print encode_utf8("yes\n") if $depends->satisfies('baz');
+ print encode_utf8("no\n") if $depends->satisfies('foo');
+
+=head1 DESCRIPTION
+
+This module provides functions for parsing and evaluating package
+relationship fields such as Depends and Recommends for binary packages and
+Build-Depends for source packages. It parses a relationship into an
+internal format and can then answer questions such as "does this
+dependency require that a given package be installed" or "is this
+relationship a superset of another relationship."
+
+A dependency line is viewed as a predicate formula. The comma separator
+means "and", and the alternatives separator means "or". A bare package
+name is the predicate "a package of this name is available". A package
+name with a version clause is the predicate "a package of this name that
+satisfies this version clause is available." Architecture restrictions,
+as specified in Policy for build dependencies, are supported and also
+checked in the implication logic unless the new_norestriction()
+constructor is used. With that constructor, architecture restrictions
+are ignored.
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item trunk
+
+=cut
+
+has trunk => (is => 'rw', default => sub { ['AND'] });
+
+=item load (RELATION)
+
+Creates a new Lintian::Relation object corresponding to the parsed
+relationship RELATION. This object can then be used to ask questions
+about that relationship. RELATION may be C<undef> or the empty string, in
+which case the returned Lintian::Relation object is empty (always
+satisfied).
+
+=cut
+
+sub load {
+ my ($self, $condition, $with_restrictions) = @_;
+
+ $condition //= $EMPTY;
+
+ my @trunk = ('AND');
+
+ my @requirements = grep { length } split(/\s*,\s*/, $condition);
+ for my $requirement (@requirements) {
+
+ my @predicates;
+
+ my @alternatives = split(/\s*\|\s*/, $requirement);
+ for my $alternative (@alternatives) {
+
+ my $predicate = Lintian::Relation::Predicate->new;
+ $predicate->parse($alternative, $with_restrictions);
+
+ push(@predicates, ['PRED', $predicate]);
+ }
+
+ push(@trunk, @predicates)
+ if @predicates == 1;
+
+ push(@trunk, ['OR', @predicates])
+ if @predicates > 1;
+ }
+
+ $self->trunk(\@trunk);
+
+ return $self;
+}
+
+=item load_norestriction (RELATION)
+
+Creates a new Lintian::Relation object corresponding to the parsed
+relationship RELATION, ignoring architecture restrictions and restriction
+lists. This should be used in cases where we only care if a dependency is
+present in some cases and we don't want to require that the architectures
+match (such as when checking for proper build dependencies, since if there
+are architecture constraints the maintainer is doing something beyond
+Lintian's ability to analyze) or that the restrictions list match (Lintian
+can't handle dependency implications with build profiles yet). RELATION
+may be C<undef> or the empty string, in which case the returned
+Lintian::Relation object is empty (always satisfied).
+
+=cut
+
+sub load_norestriction {
+ my ($self, $condition) = @_;
+
+ return $self->load($condition, $FALSE);
+}
+
+=item logical_and(RELATION, ...)
+
+Creates a new Lintian::Relation object produced by AND'ing all the
+relations together. Semantically it is the similar to:
+
+ Lintian::Relation->new (join (', ', @relations))
+
+Except it can avoid some overhead and it works if some of the elements
+are Lintian::Relation objects already.
+
+=cut
+
+sub logical_and {
+ my ($self, @conditions) = @_;
+
+ my @tree = ('AND');
+
+ # make sure to add $self
+ for my $condition (@conditions, $self) {
+
+ my $relation;
+
+ if (ref $condition eq $EMPTY) {
+ # allow string conditions
+ $relation = Lintian::Relation->new->load($condition);
+
+ } else {
+ $relation = $condition;
+ }
+
+ next
+ if $relation->is_empty;
+
+ if ( $tree[$BRANCH_TYPE] eq 'AND'
+ && $relation->trunk->[$BRANCH_TYPE] eq 'AND') {
+
+ my @anded = @{$relation->trunk};
+ shift @anded;
+ push(@tree, @anded);
+
+ } else {
+ push(@tree, $relation->trunk);
+ }
+ }
+
+ my $created = Lintian::Relation->new;
+ $created->trunk(\@tree);
+
+ return $created;
+}
+
+=item redundancies()
+
+Returns a list of duplicated elements within the relation object. Each
+element of the returned list will be a reference to an anonymous array
+holding a set of relations considered redundancies of each other. Two
+relations are considered redundancies if one satisfies the other, meaning that
+if one relationship is satisfied, the other is necessarily satisfied.
+This relationship does not have to be commutative: the opposite
+implication may not hold.
+
+=cut
+
+sub redundancies {
+ my ($self) = @_;
+
+ # there are no redundancies unless the top-level relationship is AND.
+ return ()
+ unless $self->trunk->[$BRANCH_TYPE] eq 'AND';
+
+# The logic here is a bit complex in order to merge sets of duplicate
+# dependencies. We want foo (<< 2), foo (>> 1), foo (= 1.5) to end up as
+# one set of redundancies, even though the first doesn't satisfy the second.
+#
+# $redundant_sets holds a hash, where the key is the earliest dependency in a set
+# and the value is a hash whose keys are the other dependencies in the
+# set. $seen holds a map from package names to the duplicate sets that
+# they're part of, if they're not the earliest package in a set. If
+# either of the dependencies in a duplicate pair were already seen, add
+# the missing one of the pair to the existing set rather than creating a
+# new one.
+ my %redundant_sets;
+
+ my @remaining = @{$self->trunk};
+
+ # discard AND identifier
+ shift @remaining;
+ my $i = 1;
+
+ my %seen;
+ while (@remaining > 1) {
+
+ my $branch_i = shift @remaining;
+ my $j = $i + 1;
+
+ # run against all others
+ for my $branch_j (@remaining) {
+
+ my $forward = implies_array($branch_i, $branch_j);
+ my $reverse = implies_array($branch_j, $branch_i);
+
+ if ($forward or $reverse) {
+ my $one = $self->to_string($branch_i);
+ my $two = $self->to_string($branch_j);
+
+ if ($seen{$one}) {
+ $redundant_sets{$seen{$one}}{$two} = $j;
+ $seen{$two} = $seen{$one};
+
+ } elsif ($seen{$two}) {
+ $redundant_sets{$seen{$two}}{$one} = $i;
+ $seen{$one} = $seen{$two};
+
+ } else {
+ $redundant_sets{$one} ||= {};
+ $redundant_sets{$one}{$two} = $j;
+ $seen{$two} = $one;
+ }
+ }
+ } continue {
+ $j++;
+ }
+ } continue {
+ $i++;
+ }
+
+ return map { [$_, keys %{ $redundant_sets{$_}}] } keys %redundant_sets;
+}
+
+=item restriction_less
+
+Returns a restriction-less variant of this relation.
+
+=cut
+
+sub restriction_less {
+ my ($self) = @_;
+
+ my $unrestricted
+ = Lintian::Relation->new->load_norestriction($self->to_string);
+
+ return $unrestricted;
+}
+
+=item satisfies(RELATION)
+
+Returns true if the relationship satisfies RELATION, meaning that if the
+Lintian::Relation object is satisfied, RELATION will always be satisfied.
+RELATION may be either a string or another Lintian::Relation object.
+
+By default, architecture restrictions are honored in RELATION if it is a
+string. If architecture restrictions should be ignored in RELATION,
+create a Lintian::Relation object with new_norestriction() and pass that
+in as RELATION instead of the string.
+
+=item implies_array
+
+=cut
+
+# This internal function does the heavy of AND, OR, and NOT logic. It expects
+# two references to arrays instead of an object and a relation.
+sub implies_array {
+ my ($p, $q) = @_;
+
+ my $i;
+ my $q0 = $q->[$BRANCH_TYPE];
+ my $p0 = $p->[$BRANCH_TYPE];
+
+ if ($q0 eq 'PRED') {
+ if ($p0 eq 'PRED') {
+ return $p->[$PREDICATE]->satisfies($q->[$PREDICATE]);
+ } elsif ($p0 eq 'AND') {
+ $i = 1;
+ while ($i < @{$p}) {
+ return 1 if implies_array($p->[$i++], $q);
+ }
+ return 0;
+ } elsif ($p0 eq 'OR') {
+ $i = 1;
+ while ($i < @{$p}) {
+ return 0 if not implies_array($p->[$i++], $q);
+ }
+ return 1;
+ } elsif ($p0 eq 'NOT') {
+ return implies_array_inverse($p->[1], $q);
+ }
+ } elsif ($q0 eq 'AND') {
+ # Each of q's clauses must be deduced from p.
+ $i = 1;
+ while ($i < @{$q}) {
+ return 0 if not implies_array($p, $q->[$i++]);
+ }
+ return 1;
+
+ } elsif ($q0 eq 'OR') {
+ # If p is something other than OR, p needs to satisfy one of the
+ # clauses of q. If p is an AND clause, q is satisfied if any of the
+ # clauses of p satisfy it.
+ #
+ # The interesting case is OR. In this case, do an OR to OR comparison
+ # to determine if q's clause is a superset of p's clause as follows:
+ # take each branch of p and see if it satisfies a branch of q. If
+ # each branch of p satisfies some branch of q, return 1. Otherwise,
+ # return 0.
+ #
+ # Simple logic that requires that p satisfy at least one of the
+ # clauses of q considered in isolation will miss that a|b satisfies
+ # a|b|c, since a|b doesn't satisfy any of a, b, or c in isolation.
+ if ($p0 eq 'PRED') {
+ $i = 1;
+ while ($i < @{$q}) {
+ return 1 if implies_array($p, $q->[$i++]);
+ }
+ return 0;
+ } elsif ($p0 eq 'AND') {
+ $i = 1;
+ while ($i < @{$p}) {
+ return 1 if implies_array($p->[$i++], $q);
+ }
+ return 0;
+ } elsif ($p0 eq 'OR') {
+
+ my @p_branches = @{$p};
+ shift @p_branches;
+
+ my @q_branches = @{$q};
+ shift @q_branches;
+
+ for my $p_branch (@p_branches) {
+
+ return 0
+ unless any { implies_array($p_branch, $_) }@q_branches;
+ }
+
+ return 1;
+
+ } elsif ($p->[$BRANCH_TYPE] eq 'NOT') {
+ return implies_array_inverse($p->[1], $q);
+ }
+
+ } elsif ($q0 eq 'NOT') {
+ if ($p0 eq 'NOT') {
+ return implies_array($q->[1], $p->[1]);
+ }
+ return implies_array_inverse($p, $q->[1]);
+ }
+
+ return undef;
+}
+
+# The public interface.
+sub satisfies {
+ my ($self, $condition) = @_;
+
+ my $relation;
+ if (ref $condition eq $EMPTY) {
+ # allow string conditions
+ $relation = Lintian::Relation->new->load($condition);
+
+ } else {
+ $relation = $condition;
+ }
+
+ return implies_array($self->trunk, $relation->trunk) // 0;
+}
+
+=item satisfies_inverse(RELATION)
+
+Returns true if the relationship satisfies that RELATION is certainly false,
+meaning that if the Lintian::Relation object is satisfied, RELATION cannot
+be satisfied. RELATION may be either a string or another
+Lintian::Relation object.
+
+As with satisfies(), by default, architecture restrictions are honored in
+RELATION if it is a string. If architecture restrictions should be
+ignored in RELATION, create a Lintian::Relation object with
+new_norestriction() and pass that in as RELATION instead of the string.
+
+=item implies_array_inverse
+
+=cut
+
+# This internal function does the heavily lifting for AND, OR, and NOT
+# handling for inverse implications. It takes two references to arrays and
+# returns true iff the falsehood of the second can be deduced from the truth
+# of the first.
+sub implies_array_inverse {
+ my ($p, $q) = @_;
+ my $i;
+ my $q0 = $q->[$BRANCH_TYPE];
+ my $p0 = $p->[$BRANCH_TYPE];
+ if ($q0 eq 'PRED') {
+ if ($p0 eq 'PRED') {
+ return $p->[$PREDICATE]->satisfies_inverse($q->[$PREDICATE]);
+ } elsif ($p0 eq 'AND') {
+ # q's falsehood can be deduced from any of p's clauses
+ $i = 1;
+ while ($i < @{$p}) {
+ return 1 if implies_array_inverse($p->[$i++], $q);
+ }
+ return 0;
+ } elsif ($p0 eq 'OR') {
+ # q's falsehood must be deduced from each of p's clauses
+ $i = 1;
+ while ($i < @{$p}) {
+ return 0 if not implies_array_inverse($p->[$i++], $q);
+ }
+ return 1;
+ } elsif ($p0 eq 'NOT') {
+ return implies_array($q, $p->[1]);
+ }
+ } elsif ($q0 eq 'AND') {
+ # Any of q's clauses must be falsified by p.
+ $i = 1;
+ while ($i < @{$q}) {
+ return 1 if implies_array_inverse($p, $q->[$i++]);
+ }
+ return 0;
+ } elsif ($q0 eq 'OR') {
+ # Each of q's clauses must be falsified by p.
+ $i = 1;
+ while ($i < @{$q}) {
+ return 0 if not implies_array_inverse($p, $q->[$i++]);
+ }
+ return 1;
+ } elsif ($q0 eq 'NOT') {
+ return implies_array($p, $q->[1]);
+ }
+
+ return 0;
+}
+
+# The public interface.
+sub satisfies_inverse {
+ my ($self, $condition) = @_;
+
+ my $relation;
+ if (ref $condition eq $EMPTY) {
+ # allow string conditions
+ $relation = Lintian::Relation->new->load($condition);
+
+ } else {
+ $relation = $condition;
+ }
+
+ return implies_array_inverse($self->trunk, $relation->trunk) // 0;
+}
+
+=item to_string
+
+Returns the textual form of a relationship. This converts the internal
+form back into the textual representation and returns that, not the
+original argument, so the spacing is standardized. Returns undef on
+internal failures (such as an object in an unexpected format).
+
+=cut
+
+# The second argument isn't part of the public API. It's a partial relation
+# that's not a blessed object and is used by to_string() internally so that it
+# can recurse.
+sub to_string {
+ my ($self, $branch) = @_;
+
+ my $tree = $branch // $self->trunk;
+
+ my $text;
+ if ($tree->[$BRANCH_TYPE] eq 'PRED') {
+
+ $text = $tree->[$PREDICATE]->to_string;
+
+ } elsif ($tree->[$BRANCH_TYPE] eq 'AND' || $tree->[$BRANCH_TYPE] eq 'OR') {
+
+ my $connector = ($tree->[$BRANCH_TYPE] eq 'AND') ? ', ' : ' | ';
+ my @separated = map { $self->to_string($_) } @{$tree}[1 .. $#{$tree}];
+ $text = join($connector, @separated);
+
+ } elsif ($tree->[$BRANCH_TYPE] eq 'NOT') {
+
+ # currently not generated by any relation
+ $text = '! ' . $tree->[$PREDICATE]->to_string;
+
+ } else {
+ confess encode_utf8("Case $tree->[$BRANCH_TYPE] not implemented");
+ }
+
+ return $text;
+}
+
+=item matches (REGEX[, WHAT])
+
+Check if one of the predicates in this relation matches REGEX. WHAT
+determines what is tested against REGEX and if not given, defaults to
+VISIT_PRED_NAME.
+
+This method will return a truth value if REGEX matches at least one
+predicate or clause (as defined by the WHAT parameter - see below).
+
+NOTE: Often L</satisfies> (or L</satisfies_inverse>) is a better choice
+than this method. This method should generally only be used when
+checking for a "pattern" package (e.g. phpapi-[\d\w+]+).
+
+
+WHAT can be one of:
+
+=over 4
+
+=item VISIT_PRED_NAME
+
+Match REGEX against the package name in each predicate (i.e. version
+and architecture constrains are ignored). Each predicate is tested in
+isolation. As an example:
+
+ my $rel = Lintian::Relation->new ('somepkg | pkg-0 (>= 1)');
+ # Will match (version is ignored)
+ $rel->matches (qr/^pkg-\d$/, VISIT_PRED_NAME);
+
+=item VISIT_PRED_FULL
+
+Match REGEX against the full (normalized) predicate (i.e. including
+version and architecture). Each predicate is tested in isolation.
+As an example:
+
+ my $vrel = Lintian::Relation->new ('somepkg | pkg-0 (>= 1)');
+ my $uvrel = Lintian::Relation->new ('somepkg | pkg-0');
+
+ # Will NOT match (does not match with version)
+ $vrel->matches (qr/^pkg-\d$/, VISIT_PRED_FULL);
+ # Will match (this relation does not have a version)
+ $uvrel->matches (qr/^pkg-\d$/, VISIT_PRED_FULL);
+
+ # Will match (but only because there is a version)
+ $vrel->matches (qr/^pkg-\d \(.*\)$/, VISIT_PRED_FULL);
+ # Will NOT match (there is no version in the relation)
+ $uvrel->matches (qr/^pkg-\d \(.*\)$/, VISIT_PRED_FULL);
+
+=item VISIT_OR_CLAUSE_FULL
+
+Match REGEX against the full (normalized) OR clause. Each predicate
+will have both version and architecture constrains present. As an
+example:
+
+
+ my $vpred = Lintian::Relation->new ('pkg-0 (>= 1)');
+ my $orrel = Lintian::Relation->new ('somepkg | pkg-0 (>= 1)');
+ my $rorrel = Lintian::Relation->new ('pkg-0 (>= 1) | somepkg');
+
+ # Will match
+ $vrel->matches (qr/^pkg-\d(?: \([^\)]\))?$/, VISIT_OR_CLAUSE_FULL);
+ # These Will NOT match (does not match the "|" and the "somepkg" part)
+ $orrel->matches (qr/^pkg-\d(?: \([^\)]\))?$/, VISIT_OR_CLAUSE_FULL);
+ $rorrel->matches (qr/^pkg-\d(?: \([^\)]\))?$/, VISIT_OR_CLAUSE_FULL);
+
+=back
+
+=cut
+
+sub matches {
+ my ($self, $regex, $what) = @_;
+ $what //= VISIT_PRED_NAME;
+ return $self->visit(sub { m/$regex/ }, $what | VISIT_STOP_FIRST_MATCH);
+}
+
+=item equals
+
+Same for full-string matches. Satisfies the perlcritic policy
+RegularExpressions::ProhibitFixedStringMatches.
+
+=cut
+
+sub equals {
+ my ($self, $string, $what) = @_;
+ $what //= VISIT_PRED_NAME;
+ return $self->visit(sub { $_ eq $string }, $what | VISIT_STOP_FIRST_MATCH);
+}
+
+=item visit (CODE[, FLAGS])
+
+Visit clauses or predicates of this relation. Each clause or
+predicate is passed to CODE as first argument and will be available as
+C<$_>.
+
+The optional bitmask parameter, FLAGS, can be used to control what is
+visited and such. If FLAGS is not given, it defaults to
+VISIT_PRED_NAME. The possible values of FLAGS are:
+
+=over 4
+
+=item VISIT_PRED_NAME
+
+The package name in each predicate is visited, but the version and
+architecture part(s) are left out (if any).
+
+=item VISIT_PRED_FULL
+
+The full predicates are visited in turn. The predicate will be
+normalized (by L</to_string>).
+
+=item VISIT_OR_CLAUSE_FULL
+
+CODE will be passed the full OR clauses of this relation. The clauses
+will be normalized (by L</to_string>)
+
+Note: It will not visit the underlying predicates in the clause.
+
+=item VISIT_STOP_FIRST_MATCH
+
+Stop the visits the first time CODE returns a truth value. This is
+similar to L<first|List::Util/first>, except visit will return the
+value returned by CODE.
+
+=back
+
+Except where a given flag specifies otherwise, the return value of
+visit is last value returned by CODE (or C<undef> for the empty
+relation).
+
+=cut
+
+# The last argument is not part of the public API. It's a partial
+# relation that's not a blessed object and is used by visit()
+# internally so that it can recurse.
+
+sub visit {
+ my ($self, $code, $flags, $branch) = @_;
+
+ my $tree = $branch // $self->trunk;
+ my $rel_type = $tree->[$BRANCH_TYPE];
+
+ $flags //= 0;
+
+ if ($rel_type eq 'PRED') {
+ my $predicate = $tree->[$PREDICATE];
+ my $against = $predicate->name;
+ $against = $predicate->to_string
+ if $flags & VISIT_PRED_FULL;
+
+ local $_ = $against;
+ return scalar $code->($against);
+
+ } elsif (($flags & VISIT_OR_CLAUSE_FULL) == VISIT_OR_CLAUSE_FULL
+ and $rel_type eq 'OR') {
+
+ my $against = $self->to_string($tree);
+
+ local $_ = $against;
+ return scalar $code->($against);
+
+ } elsif ($rel_type eq 'AND'
+ or $rel_type eq 'OR'
+ or $rel_type eq 'NOT') {
+
+ for my $rel (@{$tree}[1 .. $#{$tree}]) {
+ my $ret = scalar $self->visit($code, $flags, $rel);
+ if ($ret && ($flags & VISIT_STOP_FIRST_MATCH)) {
+ return $ret;
+ }
+ }
+ return 0;
+ }
+
+ return 0;
+}
+
+=item is_empty
+
+Returns a truth value if this relation is empty (i.e. it contains no
+predicates).
+
+=cut
+
+sub is_empty {
+ my ($self) = @_;
+
+ return 1
+ if $self->trunk->[$BRANCH_TYPE] eq 'AND' && !$self->trunk->[1];
+
+ return 0;
+}
+
+=item unparsable_predicates
+
+Returns a list of predicates that were unparsable.
+
+They are returned in the original textual representation and are also
+sorted by said representation.
+
+=cut
+
+sub unparsable_predicates {
+ my ($self) = @_;
+
+ my @worklist = ($self->trunk);
+ my @unparsable;
+
+ while (my $current = pop(@worklist)) {
+
+ my $rel_type = $current->[$BRANCH_TYPE];
+
+ if ($rel_type ne 'PRED') {
+
+ push(@worklist, @{$current}[1 .. $#{$current}]);
+ next;
+ }
+
+ my $predicate = $current->[$PREDICATE];
+
+ push(@unparsable, $predicate->literal)
+ unless $predicate->parsable;
+ }
+
+ my @sorted = sort @unparsable;
+
+ return @sorted;
+}
+
+=back
+
+=head1 AUTHOR
+
+Originally written by Russ Allbery <rra@debian.org> 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
diff --git a/lib/Lintian/Relation/Predicate.pm b/lib/Lintian/Relation/Predicate.pm
new file mode 100644
index 0000000..4714197
--- /dev/null
+++ b/lib/Lintian/Relation/Predicate.pm
@@ -0,0 +1,553 @@
+# -*- perl -*-
+# Lintian::Relation::Predicate -- relationship predicates
+
+# Copyright (C) 1998 Christian Schwarz and Richard Braakman
+# Copyright (C) 2004-2009 Russ Allbery <rra@debian.org>
+# Copyright (C) 2018 Chris Lamb <lamby@debian.org>
+# Copyright (C) 2020-2021 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 <http://www.gnu.org/licenses/>.
+
+package Lintian::Relation::Predicate;
+
+use v5.20;
+use warnings;
+use utf8;
+
+use Const::Fast;
+
+use Lintian::Relation::Version qw(:all);
+
+use Moo;
+use namespace::clean;
+
+const my $EMPTY => q{};
+const my $SPACE => q{ };
+const my $COLON => q{:};
+
+const my $EQUAL => q{=};
+const my $LESS_THAN => q{<};
+const my $LESS_THAN_OR_EQUAL => q{<=};
+const my $DOUBLE_LESS_THAN => q{<<};
+const my $GREATER_THAN => q{>};
+const my $GREATER_THAN_OR_EQUAL => q{>=};
+const my $DOUBLE_GREATER_THAN => q{>>};
+
+const my $LEFT_PARENS => q{(};
+const my $RIGHT_PARENS => q{)};
+const my $LEFT_SQUARE => q{[};
+const my $RIGHT_SQUARE => q{]};
+const my $LEFT_ANGLE => q{<};
+const my $RIGHT_ANGLE => q{>};
+
+const my $TRUE => 1;
+const my $FALSE => 0;
+
+=head1 NAME
+
+Lintian::Relation::Predicate - Lintian type for relationship predicates
+
+=head1 SYNOPSIS
+
+ use Lintian::Relation::Predicate;
+
+=head1 DESCRIPTION
+
+This module provides functions for parsing and evaluating package
+relationships such as Depends and Recommends for binary packages and
+Build-Depends for source packages. It parses a relationship into an
+internal format and can then answer questions such as "does this
+dependency require that a given package be installed" or "is this
+relationship a superset of another relationship."
+
+=head1 INSTANCE METHODS
+
+=over 4
+
+=item literal
+
+=item C<parsable>
+
+=item name
+
+=item multiarch_qualifier
+
+=item version_operator
+
+=item reference_version
+
+=item build_architecture
+
+=item build_profile
+
+=cut
+
+has literal => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has parsable => (is => 'rw', default => $FALSE);
+
+has name => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has multiarch_qualifier => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has version_operator => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has reference_version => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has build_architecture => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+has build_profile => (
+ is => 'rw',
+ default => $EMPTY,
+ coerce => sub { my ($text) = @_; return ($text // $EMPTY); }
+);
+
+=item parse
+
+=cut
+
+# The internal parser which converts a single package element of a
+# relationship into the parsed form used for later processing. We permit
+# substvars to be used as package names so that we can use these routines with
+# the unparsed debian/control file.
+sub parse {
+ my ($self, $text, $with_restrictions) = @_;
+
+ $with_restrictions //= $TRUE;
+
+ # store the element as-is, so we can reconstitute it later
+ $self->literal($text);
+
+ if (
+ $text =~ m{
+ ^\s* # skip leading whitespace
+ ( # package name or substvar (1)
+ (?: # start of the name
+ [a-zA-Z0-9][a-zA-Z0-9+.-]* # start of a package name
+ | # or
+ \$\{[a-zA-Z0-9:-]+\} # substvar
+ ) # end of start of the name
+ (?: # substvars may be mixed in
+ [a-zA-Z0-9+.-]+ # package name portion
+ | # or
+ \$\{[a-zA-Z0-9:-]+\} # substvar
+ )* # zero or more portions or substvars
+ ) # end of package name or substvar
+ (?:[:]([a-z0-9-]+))? # optional Multi-arch arch specification (2)
+ (?: # start of optional version
+ \s* \( # open parenthesis for version part
+ \s* (<<|<=|>=|>>|[=<>]) # relation part (3)
+ \s* ([^\)]+) # version (4)
+ \s* \) # closing parenthesis
+ )? # end of optional version
+ (?: # start of optional architecture
+ \s* \[ # open bracket for architecture
+ \s* ([^\]]+) # architectures (5)
+ \s* \] # closing bracket
+ )? # end of optional architecture
+ (?: # start of optional restriction
+ \s* < # open bracket for restriction
+ \s* ([^,]+) # don't parse restrictions now
+ \s* > # closing bracket
+ )? # end of optional restriction
+ \s* $}x
+ ) {
+ $self->parsable($TRUE);
+
+ $self->name($1);
+ $self->multiarch_qualifier($2);
+ $self->version_operator($3);
+ $self->reference_version($4);
+ $self->build_architecture($5);
+ $self->build_profile($6);
+
+ $self->reference_version($EMPTY)
+ unless length $self->version_operator;
+
+ $self->version_operator($DOUBLE_LESS_THAN)
+ if $self->version_operator eq $LESS_THAN;
+
+ $self->version_operator($DOUBLE_GREATER_THAN)
+ if $self->version_operator eq $GREATER_THAN;
+
+ unless ($with_restrictions) {
+ $self->multiarch_qualifier('any');
+ $self->version_operator($EMPTY);
+ $self->reference_version($EMPTY);
+ $self->build_architecture($EMPTY);
+ $self->build_profile($EMPTY);
+ }
+ }
+
+ return;
+}
+
+=item satisfies
+
+=cut
+
+# This internal function does the heavily lifting of comparing two
+# elements.
+#
+# Takes two elements and returns true iff the second can be deduced from the
+# first. If the second is falsified by the first (in other words, if self
+# actually satisfies not other), return 0. Otherwise, return undef. The 0 return
+# is used by implies_element_inverse.
+sub satisfies {
+ my ($self, $other) = @_;
+
+ if (!$self->parsable || !$other->parsable) {
+
+ return 1
+ if $self->to_string eq $other->to_string;
+
+ return undef;
+ }
+
+ # If the names don't match, there is no relationship between them.
+ return undef
+ if $self->name ne $other->name;
+
+ # the restriction formula forms a disjunctive normal form expression one
+ # way to check whether A <dnf1> satisfies A <dnf2> is to check:
+ #
+ # if dnf1 == dnf1 OR dnf2:
+ # the second dependency is superfluous because the first dependency
+ # applies in all cases the second one applies
+ #
+ # an easy way to check for equivalence of the two dnf expressions would be
+ # to construct the truth table for both expressions ("dnf1" and "dnf1 OR
+ # dnf2") for all involved profiles and then comparing whether they are
+ # equal
+ #
+ # the size of the truth tables grows with 2 to the power of the amount of
+ # involved profile names but since there currently only exist six possible
+ # profile names (see data/fields/build-profiles) that should be okay
+ #
+ # FIXME: we are not doing this check yet so if we encounter a dependency
+ # with build profiles we assume that one does not satisfy the other:
+
+ return undef
+ if length $self->build_profile
+ || length $other->build_profile;
+
+ # If the names match, then the only difference is in the architecture or
+ # version clauses. First, check architecture. The architectures for self
+ # must be a superset of the architectures for other.
+ my @self_arches = split($SPACE, $self->build_architecture);
+ my @other_arches = split($SPACE, $other->build_architecture);
+ if (@self_arches || @other_arches) {
+ my $self_arch_neg = @self_arches && $self_arches[0] =~ /^!/;
+ my $other_arch_neg = @other_arches && $other_arches[0] =~ /^!/;
+
+ # If self has no arches, it is a superset of other and we should fall through
+ # to the version check.
+ if (not @self_arches) {
+ # nothing
+ }
+
+ # If other has no arches, it is a superset of self and there are no useful
+ # implications.
+ elsif (not @other_arches) {
+
+ return undef;
+ }
+
+ # Both have arches. If neither are negated, we know nothing useful
+ # unless other is a subset of self.
+ elsif (not $self_arch_neg and not $other_arch_neg) {
+ my %self_arches = map { $_ => 1 } @self_arches;
+ my $subset = 1;
+ for my $arch (@other_arches) {
+ $subset = 0 unless $self_arches{$arch};
+ }
+
+ return undef
+ unless $subset;
+ }
+
+ # If both are negated, we know nothing useful unless self is a subset of
+ # other (and therefore has fewer things excluded, and therefore is more
+ # general).
+ elsif ($self_arch_neg and $other_arch_neg) {
+ my %other_arches = map { $_ => 1 } @other_arches;
+ my $subset = 1;
+ for my $arch (@self_arches) {
+ $subset = 0 unless $other_arches{$arch};
+ }
+
+ return undef
+ unless $subset;
+ }
+
+ # If other is negated and self isn't, we'd need to know the full list of
+ # arches to know if there's any relationship, so bail.
+ elsif (not $self_arch_neg and $other_arch_neg) {
+
+ return undef;
+ }
+
+# If self is negated and other isn't, other is a subset of self iff none of the
+# negated arches in self are present in other.
+ elsif ($self_arch_neg and not $other_arch_neg) {
+ my %other_arches = map { $_ => 1 } @other_arches;
+ my $subset = 1;
+ for my $arch (@self_arches) {
+ $subset = 0 if $other_arches{substr($arch, 1)};
+ }
+
+ return undef
+ unless $subset;
+ }
+ }
+
+ # Multi-arch architecture specification
+
+ # According to the spec, only the special value "any" is allowed
+ # and it is "recommended" to consider "other such package
+ # relations as unsatisfiable". That said, there seem to be an
+ # interest in supporting ":<arch>" as well, so we will (probably)
+ # have to accept those as well.
+ #
+ # Other than that, we would need to know that the package has the
+ # field "Multi-arch: allowed", but we cannot check that here. So
+ # we assume that it is okay.
+
+ # pkg has no chance of satisfing pkg:Y unless Y is 'any'
+ return undef
+ if !length $self->multiarch_qualifier
+ && length $other->multiarch_qualifier
+ && $other->multiarch_qualifier ne 'any';
+
+ # TODO: Review this case. Are there cases where other cannot
+ # disprove self due to the ":any"-qualifier? For now, we
+ # assume there are no such cases.
+ # pkg:X has no chance of satisfying pkg
+ return undef
+ if length $self->multiarch_qualifier
+ && !length $other->multiarch_qualifier;
+
+ # For now assert that only the identity holds. In practise, the
+ # "pkg:X" (for any valid value of X) seems to satisfy "pkg:any",
+ # fixing that is a TODO (because version clauses complicates
+ # matters)
+ # pkg:X has no chance of satisfying pkg:Y unless X equals Y
+ return undef
+ if length $self->multiarch_qualifier
+ && length $other->multiarch_qualifier
+ && $self->multiarch_qualifier ne $other->multiarch_qualifier;
+
+ # Now, down to version. The implication is true if self's clause is stronger
+ # than other's, or is equivalent.
+
+ # If other has no version clause, then self's clause is always stronger.
+ return 1
+ unless length $other->version_operator;
+
+# If other does have a version clause, then self must also have one to have any
+# useful relationship.
+ return undef
+ unless length $self->version_operator;
+
+ # other wants an exact version, so self must provide that exact version. self
+ # disproves other if other's version is outside the range enforced by self.
+ if ($other->version_operator eq $EQUAL) {
+ if ($self->version_operator eq $DOUBLE_LESS_THAN) {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $LESS_THAN_OR_EQUAL) {
+ return versions_lt($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $DOUBLE_GREATER_THAN) {
+ return versions_gte($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $GREATER_THAN_OR_EQUAL) {
+ return versions_gt($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $EQUAL) {
+ return versions_equal($self->reference_version,
+ $other->reference_version) ? 1 : 0;
+ }
+ }
+
+# A greater than clause may disprove a less than clause. Otherwise, if
+# self's clause is <<, <=, or =, the version must be <= other's to satisfy other.
+ if ($other->version_operator eq $LESS_THAN_OR_EQUAL) {
+ if ($self->version_operator eq $DOUBLE_GREATER_THAN) {
+ return versions_gte($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $GREATER_THAN_OR_EQUAL) {
+ return versions_gt($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $EQUAL) {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 1 : 0;
+ } else {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ }
+ }
+
+ # Similar, but << is stronger than <= so self's version must be << other's
+ # version if the self relation is <= or =.
+ if ($other->version_operator eq $DOUBLE_LESS_THAN) {
+ if ( $self->version_operator eq $DOUBLE_GREATER_THAN
+ || $self->version_operator eq $GREATER_THAN_OR_EQUAL) {
+ return versions_gte($self->reference_version,
+ $self->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $DOUBLE_LESS_THAN) {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ } elsif ($self->version_operator eq $EQUAL) {
+ return versions_lt($self->reference_version,
+ $other->reference_version) ? 1 : 0;
+ } else {
+ return versions_lt($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ }
+ }
+
+ # Same logic as above, only inverted.
+ if ($other->version_operator eq $GREATER_THAN_OR_EQUAL) {
+ if ($self->version_operator eq $DOUBLE_LESS_THAN) {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $LESS_THAN_OR_EQUAL) {
+ return versions_lt($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $EQUAL) {
+ return versions_gte($self->reference_version,
+ $other->reference_version) ? 1 : 0;
+ } else {
+ return versions_gte($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ }
+ }
+ if ($other->version_operator eq $DOUBLE_GREATER_THAN) {
+ if ( $self->version_operator eq $DOUBLE_LESS_THAN
+ || $self->version_operator eq $LESS_THAN_OR_EQUAL) {
+ return versions_lte($self->reference_version,
+ $other->reference_version) ? 0 : undef;
+ } elsif ($self->version_operator eq $DOUBLE_GREATER_THAN) {
+ return versions_gte($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ } elsif ($self->version_operator eq $EQUAL) {
+ return versions_gt($self->reference_version,
+ $other->reference_version) ? 1 : 0;
+ } else {
+ return versions_gt($self->reference_version,
+ $other->reference_version) ? 1 : undef;
+ }
+ }
+
+ return undef;
+}
+
+=item satisfies_inverse
+
+=cut
+
+# This internal function does the heavy lifting of inverse implication between
+# two elements. Takes two elements and returns true iff the falsehood of
+# the second can be deduced from the truth of the first. In other words, self
+# satisfies not other, or restated, other satisfies not self. (Since if a satisfies b, not b
+# satisfies not a.) Due to the return value of implies_element(), we can let it
+# do most of the work.
+sub satisfies_inverse {
+ my ($self, $other) = @_;
+
+ my $result = $self->satisfies($other);
+ return undef
+ if !defined $result;
+
+ return $result ? 0 : 1;
+}
+
+=item to_string
+
+=cut
+
+sub to_string {
+ my ($self) = @_;
+
+ # return the original value
+ return $self->literal
+ unless $self->parsable;
+
+ my $text = $self->name;
+
+ $text .= $COLON . $self->multiarch_qualifier
+ if length $self->multiarch_qualifier;
+
+ $text
+ .= $SPACE
+ . $LEFT_PARENS
+ . $self->version_operator
+ . $SPACE
+ . $self->reference_version
+ . $RIGHT_PARENS
+ if length $self->version_operator;
+
+ $text.= $SPACE . $LEFT_SQUARE . $self->build_architecture . $RIGHT_SQUARE
+ if length $self->build_architecture;
+
+ $text .= $SPACE . $LEFT_ANGLE . $self->build_profile . $RIGHT_ANGLE
+ if length $self->build_profile;
+
+ return $text;
+}
+
+=back
+
+=head1 AUTHOR
+
+Originally written by Russ Allbery <rra@debian.org> 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
diff --git a/lib/Lintian/Relation/Version.pm b/lib/Lintian/Relation/Version.pm
new file mode 100644
index 0000000..d3552b7
--- /dev/null
+++ b/lib/Lintian/Relation/Version.pm
@@ -0,0 +1,213 @@
+# -*- perl -*-
+# Lintian::Relation::Version -- comparison operators on Debian versions
+
+# Copyright (C) 1998 Christian Schwarz and Richard Braakman
+# Copyright (C) 2004-2009 Russ Allbery <rra@debian.org>
+# Copyright (C) 2009 Adam D. Barratt <adam@adam-barratt.org.uk>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+package Lintian::Relation::Version;
+
+use v5.20;
+use warnings;
+use utf8;
+
+use Exporter qw(import);
+
+BEGIN {
+ our @EXPORT_OK = qw(versions_equal versions_lte versions_gte versions_lt
+ versions_gt versions_compare versions_comparator);
+ our %EXPORT_TAGS = ('all' => \@EXPORT_OK);
+}
+
+use AptPkg::Config '$_config';
+use Carp qw(croak);
+use Const::Fast;
+use Unicode::UTF8 qw(encode_utf8);
+
+const my $EQUAL => q{=};
+
+my $versioning = do {
+ my $config = AptPkg::Config->new;
+ $config->init;
+ $config->system->versioning;
+};
+
+=head1 NAME
+
+Lintian::Relation::Version - Comparison operators on Debian versions
+
+=head1 SYNOPSIS
+
+ print encode_utf8("yes\n") if versions_equal('1.0', '1.00');
+ print encode_utf8("yes\n") if versions_gte('1.1', '1.0');
+ print encode_utf8("no\n") if versions_lte('1.1', '1.0');
+ print encode_utf8("yes\n") if versions_gt('1.1', '1.0');
+ print encode_utf8("no\n") if versions_lt('1.1', '1.1');
+ print encode_utf8("yes\n") if versions_compare('1.1', '<=', '1.1');
+
+=head1 DESCRIPTION
+
+This module provides five functions for comparing version numbers. The
+underlying implementation uses C<libapt-pkg-perl> to ensure that
+the results match what dpkg will expect.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item versions_equal(A, B)
+
+Returns true if A is equal to B (C<=>) and false otherwise.
+
+=cut
+
+sub versions_equal {
+ my ($p, $q) = @_;
+ my $result;
+
+ return 1 if $p eq $q;
+
+ $result = $versioning->compare($p, $q);
+
+ return ($result == 0);
+}
+
+=item versions_lte(A, B)
+
+Returns true if A is less than or equal (C<< <= >>) to B and false
+otherwise.
+
+=cut
+
+sub versions_lte {
+ my ($p, $q) = @_;
+ my $result;
+
+ return 1 if $p eq $q;
+
+ $result = $versioning->compare($p, $q);
+
+ return ($result <= 0);
+}
+
+=item versions_gte(A, B)
+
+Returns true if A is greater than or equal (C<< >= >>) to B and false
+otherwise.
+
+=cut
+
+sub versions_gte {
+ my ($p, $q) = @_;
+ my $result;
+
+ return 1 if $p eq $q;
+
+ $result = $versioning->compare($p, $q);
+
+ return ($result >= 0);
+}
+
+=item versions_lt(A, B)
+
+Returns true if A is less than (C<<< << >>>) B and false otherwise.
+
+=cut
+
+sub versions_lt {
+ my ($p, $q) = @_;
+ my $result;
+
+ return 0 if $p eq $q;
+
+ $result = $versioning->compare($p, $q);
+
+ return ($result < 0);
+}
+
+=item versions_gt(A, B)
+
+Returns true if A is greater than (C<<< >> >>>) B and false otherwise.
+
+=cut
+
+sub versions_gt {
+ my ($p, $q) = @_;
+ my $result;
+
+ return 0 if $p eq $q;
+
+ $result = $versioning->compare($p, $q);
+
+ return ($result > 0);
+}
+
+=item versions_compare(A, OP, B)
+
+Returns true if A OP B, where OP is one of C<=>, C<< <= >>, C<< >= >>,
+C<<< << >>>, or C<<< >> >>>, and false otherwise.
+
+=cut
+
+sub versions_compare {
+ my ($p, $op, $q) = @_;
+ if ($op eq $EQUAL) { return versions_equal($p, $q) }
+ elsif ($op eq '<=') { return versions_lte($p, $q) }
+ elsif ($op eq '>=') { return versions_gte($p, $q) }
+ elsif ($op eq '<<') { return versions_lt($p, $q) }
+ elsif ($op eq '>>') { return versions_gt($p, $q) }
+ else { croak encode_utf8("unknown operator $op") }
+}
+
+=item versions_comparator (A, B)
+
+Returns -1, 0 or 1 if the version A is (respectively) less than, equal
+to or greater than B. This is useful for (e.g.) sorting a list of
+versions:
+
+ foreach my $version (sort versions_comparator @versions) {
+ ...
+ }
+
+=cut
+
+# Use a prototype to avoid confusing Perl when used with sort.
+
+sub versions_comparator {
+ my ($p, $q) = @_;
+ return $versioning->compare($p, $q);
+}
+
+=back
+
+=head1 AUTHOR
+
+Originally written by Russ Allbery <rra@debian.org> for Lintian and adapted
+to use libapt-pkg-perl by Adam D. Barratt <adam@adam-barratt-org.uk>.
+
+=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