#! /usr/bin/perl

# $OpenBSD: register-plist,v 1.16 2017/09/18 13:01:43 espie Exp $
# Copyright (c) 2005,2012
# Marc Espie.  All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Neither the name of OpenBSD nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY ITS AUTHOR AND THE OpenBSD project ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

use strict;
use warnings;

use OpenBSD::State;
use OpenBSD::PackageInfo;
use OpenBSD::PackingList;
use OpenBSD::Paths;

package OpenBSD::PackingList;

sub write_mogrified
{
	my ($self, $fh, $state) = @_;
	$self->visit('write_mogrified', $fh, $state);

}

sub forget_details
{
	my $self = shift;
	undef $self->{"digital-signature"};
	undef $self->{vendor};
	undef $self->{signer};
	# XXX for now we forget version because it can be there or not
	# this is not accurate, but simple
	undef $self->{version};

	my $l = $self->{items};
	if ($l->[@$l-1]->isa('OpenBSD::PackingElement::Cwd') &&
		$l->[@$l-1]->{name} eq '.') {
	    pop @$l;
	}
	$self->visit('forget_details');
}

package OpenBSD::PackingElement;

sub write_mogrified
{
	my ($self, $fh, $state) = @_;
	if ($state->{mogrified}{$self}) {
		$state->{mogrified}{$self}->write($fh);
	} else {
		$self->write($fh);
	}
}

sub forget_details
{
}

sub forget_more_details
{
}

sub flatten
{
	my ($self, $l) = @_;
	push(@$l, $self);
}

sub compare
{
	my ($self, $self2) = @_;
	my ($data, $data2);
	open(my $fh, '>',  \$data);
	open(my $fh2, '>',  \$data2);
	$self->write($fh);
	$self2->write($fh2);
	close($fh);
	close($fh2);
	if ($data ne $data2) {
		return 1;
	} else {
		return 0;
	}
}

sub remove_auxiliary
{
	return 0;
}

sub record_generic_depends
{
}

sub find_mogrified
{
}

package OpenBSD::PackingElement::DigitalSignature;
sub flatten
{
}

package OpenBSD::PackingElement::Old;
sub flatten
{
	my ($self, $l) = @_;
	if ($self->{keyword} eq 'ignore') {
		return;
	}
	$self->SUPER::flatten($l);
}

package OpenBSD::PackingElement::FileBase;

sub forget_details
{
	my $self = shift;
	undef $self->{d};
	undef $self->{md5};
	undef $self->{size};
	undef $self->{ts};
}

sub forget_more_details
{
	my $self = shift;
	undef $self->{symlink};
	undef $self->{link};
}

package OpenBSD::PackingElement::SpecialFile;
sub forget_details
{
	my $self = shift;
#	undef $self->{md5};
	undef $self->{size};
}

sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return 1;
	}
	if ($self->{name} ne $self2->{name}) {
		return 1;
	}
	if (defined $self->{d} && defined $self2->{d}) {
		if (ref($self->{d}) ne ref($self2->{d})) {
			return 1;
		}
		if ($self->{d}->equals($self2->{d})) {
			return 0;
		} else {
			return 1;
		}
	} elsif (!defined $self->{d} && !defined $self2->{d}) {
		return 0;
	}
	return 2;
}

package OpenBSD::PackingElement::Depend;

sub record_generic_depends
{
	my ($self, $state) = @_;
	$state->{stash}->{$self->forgetful_key} = $self;
}

sub find_mogrified
{
	my ($s1, $state) = @_;
	my $s2 = $state->{stash}->{$s1->forgetful_key};
	if (defined $s2 && $s1->compare($s2) == 2) {
		$state->{mogrified}{$s1} = $s2;
	}
}

package OpenBSD::PackingElement::Dependency;
sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return 1;
	}
	if ($self->{pkgpath} ne $self2->{pkgpath}) {
		return 1;
	}

	my $c = OpenBSD::PackageName->from_string($self->{def})->compare(
	    OpenBSD::PackageName->from_string($self2->{def}));
	if (!defined $c || $c < 0) {
		return 1;
	}
	if ($c == 0) {
		return 0;
	}
	return 2;
}

sub forgetful_key
{
	my $self = shift;
	$self->{name} =~ s/\-\=(\d)/\-$1/;
	return join(':', 'depend', $self->{name}, $self->{pkgpath}, $self->{pattern});
}

package OpenBSD::PackingElement::Wantlib;

sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return 1;
	}
	my $c = $self->spec->compare($self2->spec);
	if (!defined $c || $c < 0) {
		return 1;
	}
	if ($c == 0) {
		return 0;
	}
	return 2;
}

sub forgetful_key
{
	my $self = shift;
	return $self->spec->key;
}

package OpenBSD::PackingElement::Comment;

sub forget_details
{
	my $self = shift;

	if ($self->{name} =~ m/^VARS:/) {
		$self->{name} = 'VARS';
	}
}

package OpenBSD::PackingElement::CVSTag;

# XXX needed to avoid CVS expansion

our $openbsd = 'OpenBSD';

sub forget_details
{
	my $self = shift;
	$self->{name} =~ s/^(\$$openbsd: .*,v).*/$1\$/;
}

sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return 1;
	}
	if ($self->{name} eq $self2->{name}) {
		return 0;
	}
	if ($self->{name} eq "\$$openbsd\$" or 
	    $self2->{name} eq "\$$openbsd\$") {
		return 2;
	}
	return 1;
}

package OpenBSD::PackingElement::InfoFile;

sub remove_auxiliary
{
	my ($self, $list) = @_;

	my $stem = $self->{name};
	my $i;
	for ($i = 1; @$list > 0 && $list->[0]->{name} eq "$stem-$i"; $i++) {
		shift @$list;
	}
	return $i;
}

package OpenBSD::PackingElement::Manpage;

sub remove_auxiliary
{
	my ($self, $list) = @_;
	if ($self->is_source && @$list > 0 && 
	    ref($list->[0]) eq ref($self) && 
	    $list->[0]->{name} eq $self->source_to_dest) {
		shift @$list;
		return 1;
	}
	return 0;
}

sub compare
{
	my ($self, $self2) = @_;

	# both elements must be manpages
	if (ref($self) ne ref($self2)) {
		return 1;
	}

	# identical
	if ($self->{name} eq $self2->{name}) {
		return 0;
	}

	# one is the conversion of the other
	if ($self->is_source && $self->source_to_dest eq $self2->{name}) {
		return 2;
	}
	if ($self2->is_source && $self2->source_to_dest eq $self->{name}) {
		return 2;
	}

	# or they're different beasts.
	return 1;
}

package OpenBSD::PackingElement::ExtraInfo;

sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return 1;
	}
	if ($self->{ftp} ne $self2->{ftp} or 
	    $self->{cdrom} ne $self2->{cdrom}) {
		return 1;
	}
	if ($self->{subdir} eq $self2->{subdir}) {
		return 0;
	}
	if ($self->{subdir} =~ m/^mystuff\// && $self2->{subdir} eq $') {
		return 2;
	}
	if ($self2->{subdir} =~ m/^mystuff\// && $self->{subdir} eq $') {
		return 2;
	}
	return 1;
}

package main;

sub my_compare
{
	my ($p, $p2, $state) = @_;
	my $l = [];
	my $l2 = [];
	my $final = 0;
	$p->flatten($l);
	$p2->flatten($l2);
	while (my $e = shift @$l) {
		my $e2 = shift @$l2;
		return 1 unless defined $e2;
		my $r = $e->compare($e2);
		if ($r == 1) {
			return $r;
		}
		if ($r == 2) {
			push(@{$state->{updates}}, [$e2, $e]);
			$state->{mogrified}{$e} = $e2;
			$final = 2;
		}
		# zap extra info-* files and man pages
		if ($e->remove_auxiliary($l) != $e2->remove_auxiliary($l2)) {
			$final = 2;
		}
	}
	if (@$l2 > 0) {
		return 1;
	}
	return $final;
}

sub more_mogrified
{
	my ($p1, $p2, $state) = @_;

	$p2->record_generic_depends($state);
	$p1->find_mogrified($state);
}

sub act_on_compare
{
	my ($r, $p1, $p2, $result, $state) = @_;

	if ($r == 1) {
		my $t = "$result-new";
		more_mogrified($p1, $p2, $state);
		$state->errsay("Error: change in plist");
		$state->errsay("| If the old and new builds were done correctly");
		$state->errsay("| (fully up-to-date ports tree including relevant MODULES)");
		$state->errsay("| then someone probably forgot to bump a REVISION.");
		$state->errsay("| (see bsd.port.mk(5), PACKAGE_REPOSITORY)");
		if (open(my $fh, '>', $t)) {
			$p1->write_mogrified($fh, $state);
			close($fh);
			system {OpenBSD::Paths->diff} ('diff', 
			    '-L', $result, '-L', $t, '-u', $result, $t);
		} else {
			open(my $fh, "|-", OpenBSD::Paths->diff, 
			    '-L', $result, '-L', $t,
			    '-u', $result, '-');
			$p1->write_mogrified($fh, $state);
			close($fh);
		}
		return 1;
	}
	if ($r == 2) {
		$p1->tofile($result);
		$state->errsay("#1 was updated", $result);
		for my $i (@{$state->{updates}}) {
			$state->errsay("#1 -> #2",
			    $i->[0]->stringize, 
			    $i->[1]->stringize);
		}
	}
	return 0;
}

sub compare_lists
{
	my ($p1, $p2, $result, $state) = @_;
	my $r = my_compare($p1, $p2, $state);
	return act_on_compare($r, $p1, $p2, $result, $state);
}

sub compare_versions
{
	my ($dir, $plist, $state) = @_;

	opendir (my $dirhandle, $dir) or return 0;
	my (@parsed) = OpenBSD::PackageName::splitname($plist->pkgname);
	$parsed[1] = '*';
	my $reference = join('-', @parsed);
	my $n = OpenBSD::PackageName->from_string($plist->pkgname);
	my $result = 0;
	my $re = qr{^\Q$parsed[0]\E\-\d};
	my $matched_pkgpath = 0;
	my @unmatched_pkgpaths = ();
	while (my $d = readdir $dirhandle) {
		next unless $d =~ $re;
		my (@cmp) = OpenBSD::PackageName::splitname($d);
		$cmp[1] = '*';
		next if  join('-', @cmp) ne $reference;

		my $p2 = OpenBSD::PackingList->fromfile("$dir/$d",
			\&OpenBSD::PackingList::ExtraInfoOnly);
		if (!$plist->match_pkgpath($p2)) {
			push(@unmatched_pkgpaths, $d);
			next;
		}
		$matched_pkgpath = 1;
		my $n2 = OpenBSD::PackageName->from_string($p2->pkgname);
		my $c = $n->compare($n2);
		if ($c < 0) {
			$state->errsay("Found newer package #1 in #2", 
			    $p2->pkgname, $dir);
			$result = 1;
		} elsif ($c == 0) {
			$state->errsay("Found package with different name that compares equal in #2: #1", 
			    $p2->pkgname, $dir);
			$result = 1;
		}
	}
	if (!$matched_pkgpath && @unmatched_pkgpaths) {
		$state->errsay("Found package with matching name, but with different pkgpath: #1" , 
			join(' ', sort @unmatched_pkgpaths));
	}
	return $result;
}

my $state = OpenBSD::State->new('register-plist');
$state->{signature_style} = 'unsigned';
$state->handle_options('tp', '[-t p1 p2] dir pkg ...');
if (@ARGV < 2 && !$state->opt('p')) {
	$state->usage;
}

if ($state->opt('t')) {
	if (@ARGV != 2) {
		$state->usage("-t takes exactly two parameters");
	}
	my $plist = OpenBSD::PackingList->fromfile($ARGV[0]);
	my $result = $ARGV[1];
	my $plist2 = OpenBSD::PackingList->fromfile($result);
	$plist->forget_details;
	exit(compare_lists($plist, $plist2, $result, $state));
}

my @dirs = split(/:/, shift);
if (!-d $dirs[0]) {
	$state->usage("not a directory: #1", $dirs[0]);
}

if ($state->opt('p')) {
	my $plist = OpenBSD::PackingList->read(\*STDIN);
	$plist->forget_details;
	for my $dir (@dirs) {
		next unless -d $dir;
		my $result = $dir.'/'.$plist->pkgname;
		if (-f $result) {
			my $plist2 = OpenBSD::PackingList->fromfile($result);
			$plist2->forget_details;
			$plist2->forget_more_details;
			my $r = my_compare($plist, $plist2, $state);
			if ($r == 2) {
				$r = 0;
			}
			exit(act_on_compare($r, $plist, $plist2, $result, 
			    $state));
		}
	}
	exit(1);
}

my $error =0;

for my $pkgfile (@ARGV) {
	my $pkg = $state->repo->find($pkgfile);
	if (!$pkg) {
		$state->fatal("Bad package #1", $pkgfile);
	}

	my $plist = $pkg->plist;
	$pkg->close;
	$pkg->wipe_info;

	$plist->forget_details;
	for my $dir (@dirs) {
		next unless -d $dir;
		my $result = $dir.'/'.$plist->pkgname;
		if (-f $result && -s _) {
			my $plist2 = OpenBSD::PackingList->fromfile($result);
			$error += compare_lists($plist, $plist2, $result,
			    $state);
			last;
		}
		$error += compare_versions($dir, $plist, $state);
	}
	if (!$error) {
		my $result = $dirs[0].'/'.$plist->pkgname;
		if (!-f $result || -z _) {
			$plist->tofile($result);
		}
	}
}
exit($error != 0);
