#! /usr/bin/perl

# $OpenBSD: register-plist,v 1.22 2019/05/28 19:41:48 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 Result;
use constant {
	Equal => 0,
	Different => 1,
	Older => 1,
	Updatable => 2,
	StopCompare => 3,
	UpdatableButShowDiff => 4
};

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};

	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
{
}

# packing-lists in packages have more info (symlinks) that we can't
# reproduce when we test packing-lists from source
sub forget_more_details
{
}

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

sub compare
{
	my ($self, $self2) = @_;
	if (ref($self) ne ref($self2)) {
		return Result::Different;
	} else {
		return $self->compare_more($self2);
	}
}

sub compare_more
{
	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 Result::Different;
	} else {
		return Result::Equal;
	}
}

sub remove_auxiliary
{
	return 0;
}

sub record_generic_depends
{
}

sub find_mogrified
{
}

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

package OpenBSD::PackingElement::Version;
sub compare_more
{
	my ($self, $self2) = @_;
	# no version element in #2, let's mogrify
	if ($self2->name == -1) {
		return Result::Updatable;
	}
	if ($self->name == $self2->name) {
		return Result::Equal;
	}
	# versions shouldn't ever go back
	if ($self->name < $self2->name) {
		return Result::Older;
	}
	# special case: no need for further compare
	# this is a full replacement
	return Result::StopCompare;
}

# special version, avoids writing synthetic version into plist_db
sub write_mogrified
{
	my ($self, $fh, $state) = @_;
	if ($state->{mogrified}{$self}) {
		return if $state->{mogrified}{$self}->name == -1;
		$state->{mogrified}{$self}->write($fh);
	} else {
		$self->write($fh);
	}
}
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_more
{
	my ($self, $self2) = @_;
	if ($self->{name} ne $self2->{name}) {
		return Result::Different;
	}
	if (defined $self->{d} && defined $self2->{d}) {
		if (ref($self->{d}) ne ref($self2->{d})) {
			return Result::Different;
		}
		if ($self->{d}->equals($self2->{d})) {
			return Result::Equal;
		} else {
			return Result::Different;
		}
	} elsif (!defined $self->{d} && !defined $self2->{d}) {
		return Result::Equal;
	}
	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::Depend;
sub make_result
{
	my ($self, $c) = @_;
	if (!defined $c) {
		return Result::Different;
	} elsif ($c < 0) {
		return Result::Older;
	} elsif ($c == 0) {
		return Result::Equal;
	} else {
		return Result::Updatable;
	}
}

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

	return $self->make_result(
	    OpenBSD::PackageName->from_string($self->{def})->compare(
	    OpenBSD::PackageName->from_string($self2->{def})));
}

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_more
{
	my ($self, $self2) = @_;
	my $c = 
	return $self->make_result($self->spec->compare($self2->spec));
}

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_more
{
	my ($self, $self2) = @_;
	if ($self->{name} eq $self2->{name}) {
		return Result::Equal;
	}
	if ($self->{name} eq "\$$openbsd\$" or 
	    $self2->{name} eq "\$$openbsd\$") {
		return Result::Updatable;
	}
	return Result::Different;
}

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_more
{
	my ($self, $self2) = @_;

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

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

	# or they're different beasts.
	return Result::Different;
}

package OpenBSD::PackingElement::ExtraInfo;

sub compare_more
{
	my ($self, $self2) = @_;
	if ($self->{ftp} ne $self2->{ftp}) {
		return Result::Different;
	}
	my $r = Result::Equal;
	if (!defined $self->{cdrom} && defined $self2->{cdrom}) {
		$r = Result::Updatable;
	} elsif (defined $self->{cdrom} && !defined $self2->{cdrom}) {
		$r = Result::Updatable;
	} elsif (defined $self->{cdrom} && defined $self2->{cdrom}) {
		if ($self->{cdrom} ne $self2->{cdrom}) {
			return Result::Different;
		}
	}
	if ($self->{subdir} eq $self2->{subdir}) {
		return $r;
	}
	if ($self->{subdir} =~ m/^mystuff\// && $self2->{subdir} eq $') {
		return Result::Updatable;
	}
	if ($self2->{subdir} =~ m/^mystuff\// && $self->{subdir} eq $') {
		return Result::Updatable;
	}
	return Result::Different;
}

package main;

sub combine
{
	my ($comparison_result, $seen_stop) = @_;
	if (!$seen_stop) {
		return $comparison_result;
	} elsif ($comparison_result == Result::Different) {
		return Result::UpdatableButShowDiff;
	} else {
		return Result::Updatable;
	}
}

# my_compare is asymetric  $p is always the newer list
sub my_compare
{
	my ($p, $p2, $state) = @_;
	# back compatibility: if the old list doesn't have a version
	# marker, let's put a -1 there
	if (!defined $p2->{version}) {
		OpenBSD::PackingElement::Version->add($p2, "-1");
	}
	# ... and actually put a 0 in the new one, for arch-independent
	# packages
	if (!defined $p->{version}) {
		OpenBSD::PackingElement::Version->add($p, "0");
	}
	my $l = [];
	my $l2 = [];
	my $seen_stop = Result::Equal;
	my $final = Result::Equal;
	$p->flatten($l);
	$p2->flatten($l2);
	while (my $e = shift @$l) {
		my $e2 = shift @$l2;
		if (!defined $e2) {
			return combine(Result::Different, $seen_stop);
		}
		my $r = $e->compare($e2);
		if ($r == Result::Different) {
			return combine($r, $seen_stop);
		}
		if ($r == Result::StopCompare) {
			push(@{$state->{updates}}, [$e2, $e]);
			$seen_stop = $r;
		}
		if ($r == Result::Updatable) {
			push(@{$state->{updates}}, [$e2, $e]);
			$state->{mogrified}{$e} = $e2;
			$final = $r;
		}
		# zap extra info-* files and man pages
		if ($e->remove_auxiliary($l) != $e2->remove_auxiliary($l2)) {
			$final = Result::Updatable;
		}
	}
	if (@$l2 > 0) {
		return combine(Result::Different, $seen_stop);
	}
	return combine($final, $seen_stop);
}

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

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

sub error_blurb
{
	my ($state, $p) = @_;
	$state->errsay("Error: change in plist");
	$state->errsay("| Assuming 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)");

	return if defined $p->{version} && $p->{version}->name != -1;
	$state->errsay("|");
	$state->errsay("| Note that registered plist doesn't have \@version info");
	$state->errsay("| So we can't know anything about \@version bumps");
}


sub write_out_diff
{
	my ($p1, $p2, $result, $state) = @_;
	my $t = "$result-new";
	# try to actually write the bad file
	# and run a physical diff
	my $fh;
	my $pipe;
	if (open($fh, '>', $t)) {
		$pipe = 0;
	} else {
		open($fh, "|-", OpenBSD::Paths->diff, 
		    '-L', $result, '-L', $t,
		    '-u', $result, '-');
	    	$pipe = 1;
	}
	# XXX we "reverse tweak" the new file
	# to remove the parts that don't warrant the error
	$p1->write_mogrified($fh, $state);
	close($fh);
	if (!$pipe) {
		system {OpenBSD::Paths->diff} ('diff', 
		    '-L', $result, '-L', $t, '-u', $result, $t);
	}
}

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

	if ($r == Result::Different) {
		more_mogrified($p1, $p2, $state);
		error_blurb($state, $p2);
		write_out_diff($p1, $p2, $result, $state);
		return 1;
	}
	if ($r == Result::Updatable) {
		$p1->tofile($result);
		$state->errsay("#1 was updated", $result);
		for my $i (@{$state->{updates}}) {
			$state->errsay("#1 -> #2",
			    $i->[0]->fullstring, 
			    $i->[1]->fullstring);
		}
	}
	if ($r == Result::UpdatableButShowDiff) {
		$state->errsay("#1 was updated", $result);
		write_out_diff($p1, $p2, $result, $state);
		$p1->tofile($result);
	}
	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 == Result::Updatable) {
				$r = Result::Equal;
			}
			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);
