#! /usr/bin/perl

# ex:ts=8 sw=4:
# $OpenBSD: PkgAdd.pm,v 1.138 2022/07/29 14:26:45 espie Exp $
#
# Copyright (c) 2003-2014 Marc Espie <espie@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;

use OpenBSD::AddDelete;

package OpenBSD::PackingList;

sub uses_old_libs
{
	my ($plist, $state) = @_;
	require OpenBSD::RequiredBy;

	if (grep {/^\.libs\d*\-/o}
	    OpenBSD::Requiring->new($plist->pkgname)->list) {
	    	$state->say("#1 still uses old .libs",  $plist->pkgname)
		    if $state->verbose >= 3;
		return 1;
	} else {
	    	return 0;
	}
}

sub has_different_sig
{
	my ($plist, $state) = @_;
	if (!defined $plist->{different_sig}) {
		my $n = 
		    OpenBSD::PackingList->from_installation($plist->pkgname, 
			\&OpenBSD::PackingList::UpdateInfoOnly)->signature;
		my $o = $plist->signature;
		my $r = $n->compare($o, $state);
		$state->print("Comparing full signature for #1 \"#2\" vs. \"#3\":",
		    $plist->pkgname, $o->string, $n->string)
			if $state->verbose >= 3;
		if (defined $r) {
			if ($r == 0) {
				$plist->{different_sig} = 0;
				$state->say("equal") if $state->verbose >= 3;
			} elsif ($r > 0) {
				$plist->{different_sig} = 1;
				$state->say("greater") if $state->verbose >= 3;
			} else {
				$plist->{different_sig} = 1;
				$state->say("less") if $state->verbose >= 3;
			}
		} else {
			$plist->{different_sig} = 1;
			$state->say("non comparable") if $state->verbose >= 3;
		}
	}
	return $plist->{different_sig};
}

package OpenBSD::PackingElement;
sub hash_files
{
}
sub tie_files
{
}

package OpenBSD::PackingElement::FileBase;
sub hash_files
{
	my ($self, $state, $sha) = @_;
	return if $self->{link} or $self->{symlink} or $self->{nochecksum};
	if (defined $self->{d}) {
		$sha->{$self->{d}->key}{$self->name} = $self;
	}
}

sub tie_files
{
	my ($self, $state, $sha) = @_;
	return if $self->{link} or $self->{symlink} or $self->{nochecksum};
	# XXX python doesn't like this, overreliance on timestamps

	return if $self->{name} =~ m/\.py$/ && !defined $self->{ts};

	my $h = $sha->{$self->{d}->key};
	return if !defined $h;

	my ($tied, $realname);
	my $c = $h->{$self->name};
	# first we try to match with the same name
	if (defined $c) {
		$realname = $c->realname($state);
		# don't tie if the file doesn't exist
		if (-f $realname && 
		# or was altered
		    (stat _)[7] == $self->{size}) {
			$tied = $c;
		}
	}
	# otherwise we grab any other match under similar rules
	if (!defined $tied) {
		for my $c ( values %{$h} ) {
			$realname = $c->realname($state);
			next unless -f $realname;
			next unless (stat _)[7] == $self->{size};
			$tied = $c;
			last;
		}
	}
	return if !defined $tied;

	if ($state->defines('checksum')) {
		my $d = $self->compute_digest($realname, $self->{d});
		# XXX we don't have to display anything here
		# because delete will take care of that
		return unless $d->equals($self->{d});
	}
	# so we found a match that find_extractible will use
	$self->{tieto} = $tied;
	# and we also need to tell size computation we won't be needing 
	# extra diskspace for this.
	$tied->{tied} = 1;
	$state->say("Tying #1 to #2", $self->stringize, $realname) 
	    if $state->verbose >= 3;
}

package OpenBSD::PkgAdd::State;
our @ISA = qw(OpenBSD::AddDelete::State);

sub handle_options
{
	my $state = shift;
	$state->SUPER::handle_options('druUzl:A:P:',
	    '[-adcinqrsUuVvxz] [-A arch] [-B pkg-destdir] [-D name[=value]]',
	    '[-L localbase] [-l file] [-P type] pkg-name ...');

	$state->{arch} = $state->opt('A');

	if ($state->opt('P')) {
		if ($state->opt('P') eq 'ftp') {
			$state->{ftp_only} = 1;
		}
		else {
		    $state->usage("bad option: -P #1", $state->opt('P'));
		}
	}
	$state->{hard_replace} = $state->opt('r');
	$state->{newupdates} = $state->opt('u') || $state->opt('U');
	$state->{allow_replacing} = $state->{hard_replace} ||
	    $state->{newupdates};
	$state->{pkglist} = $state->opt('l');
	$state->{update} = $state->opt('u');
	$state->{fuzzy} = $state->opt('z');
	$state->{debug_packages} = $state->opt('d');
	if ($state->defines('snapshot')) {
		$state->{subst}->add('snap', 1);
	}

	if (@ARGV == 0 && !$state->{update} && !$state->{pkglist}) {
		$state->usage("Missing pkgname");
	}
}

OpenBSD::Auto::cache(cache_directory,
	sub {
		my $self = shift;
		if (defined $ENV{PKG_CACHE}) {
			return $ENV{PKG_CACHE};
		} else {
			return undef;
		}
	});

OpenBSD::Auto::cache(debug_cache_directory,
	sub {
		my $self = shift;
		if (defined $ENV{DEBUG_PKG_CACHE}) {
			return $ENV{DEBUG_PKG_CACHE};
		} else {
			return undef;
		}
	});

sub set_name_from_handle
{
	my ($state, $h, $extra) = @_;
	$extra //= '';
	$state->log->set_context($extra.$h->pkgname);
}

sub updateset
{
	my $self = shift;
	require OpenBSD::UpdateSet;

	return OpenBSD::UpdateSet->new($self);
}

sub updateset_with_new
{
	my ($self, $pkgname) = @_;

	return $self->updateset->add_newer(
	    OpenBSD::Handle->create_new($pkgname));
}

sub updateset_from_location
{
	my ($self, $location) = @_;

	return $self->updateset->add_newer(
	    OpenBSD::Handle->from_location($location));
}

sub display_timestamp
{
	my ($state, $pkgname, $timestamp) = @_;
	$state->say("#1 signed on #2", $pkgname, $timestamp);
}

OpenBSD::Auto::cache(updater,
    sub {
	require OpenBSD::Update;
	return OpenBSD::Update->new;
    });

OpenBSD::Auto::cache(tracker,
    sub {
	require OpenBSD::Tracker;
	return OpenBSD::Tracker->new;
    });

sub tweak_header
{
	my ($state, $info) = @_;
	my $header = $state->{setheader};

	if (defined $info) {
		$header.=" ($info)";
	}

	if (!$state->progress->set_header($header)) {
		return unless $state->verbose;
		if (!defined $info) {
			$header = "Adding $header";
		}
		if (defined $state->{lastheader} &&
		    $header eq $state->{lastheader}) {
			return;
		}
		$state->{lastheader} = $header;
		$state->print("#1", $header);
		$state->print("(pretending) ") if $state->{not};
		$state->print("\n");
	}
}

package OpenBSD::ConflictCache;
our @ISA = (qw(OpenBSD::Cloner));
sub new
{
	my $class = shift;
	bless {done => {}, c => {}}, $class;
}

sub add
{
	my ($self, $handle, $state) = @_;
	return if $self->{done}{$handle};
	$self->{done}{$handle} = 1;
	for my $conflict (OpenBSD::PkgCfl::find_all($handle, $state)) {
		$self->{c}{$conflict} = 1;
	}
}

sub list
{
	my $self = shift;
	return keys %{$self->{c}};
}

sub merge
{
	my ($self, @extra) = @_;
	$self->clone('c', @extra);
	$self->clone('done', @extra);
}

package OpenBSD::UpdateSet;
use OpenBSD::PackageInfo;
use OpenBSD::Handle;

sub setup_header
{
	my ($set, $state, $handle, $info) = @_;

	my $header = $state->deptree_header($set);
	if (defined $handle) {
		$header .= $handle->pkgname;
	} else {
		$header .= $set->print;
	}

	$state->{setheader} = $header;

	$state->tweak_header($info);
}

my $checked = {};

sub check_security
{
	my ($set, $state, $plist, $h) = @_;
	return if $checked->{$plist->fullpkgpath};
	$checked->{$plist->fullpkgpath} = 1;
	return if $set->{quirks};
	my ($error, $bad);
	$state->run_quirks(
		sub {
			my $quirks = shift;
			return unless $quirks->can("check_security");
			$bad = $quirks->check_security($plist->fullpkgpath);
			if (defined $bad) {
				require OpenBSD::PkgSpec;
				my $spec = OpenBSD::PkgSpec->new($bad);
				my $r = $spec->match_locations([$h->{location}]);
				if (@$r != 0) {
					$error++;
				}
			}
		});
	if ($error) {
		$state->errsay("Package #1 found, matching insecure #2", 
		    $h->pkgname, $bad);
	}
}

sub display_timestamp
{
	my ($pkgname, $plist, $state) = @_;

	return unless $plist->is_signed;
	$state->display_timestamp($pkgname,
	    $plist->get('digital-signature')->iso8601);
}

sub find_kept_handle
{
	my ($set, $n, $state) = @_;
	my $plist = $n->dependency_info;
	return if !defined $plist;
	my $pkgname = $plist->pkgname;
	if ($set->{quirks}) {
		$n->{location}->decorate($plist);
		display_timestamp($pkgname, $plist, $state);
	}
	# condition for no update
	unless (is_installed($pkgname) &&
	    (!$state->{allow_replacing} ||
	      !$state->defines('installed') &&
	      !$plist->has_different_sig($state) &&
	      !$plist->uses_old_libs($state))) {
	      	$set->check_security($state, $plist, $n);
	      	return;
	}
	my $o = $set->{older}{$pkgname};
	if (!defined $o) {
		$o = OpenBSD::Handle->create_old($pkgname, $state);
		if (!defined $o->pkgname) {
			$state->{bad}++;
			$set->cleanup(OpenBSD::Handle::CANT_INSTALL, 
			    "Bogus package already installed");
		    	return;
		}
	}
	$set->check_security($state, $plist, $o);
	if ($set->{quirks}) {
		# The installed package has inst: for a location, we want
		# the newer one (which is identical)
		$n->location->{repository}->setup_cache($state->{setlist});
	}
	$set->move_kept($o);
	$o->{tweaked} =
	    OpenBSD::Add::tweak_package_status($pkgname, $state);
	$state->updater->progress_message($state, "No change in $pkgname");
	if (defined $state->debug_cache_directory) {
		OpenBSD::PkgAdd->may_grab_debug_for($pkgname, 1, $state);
	}
	delete $set->{newer}{$pkgname};
	$n->cleanup;
}

sub figure_out_kept
{
	my ($set, $state) = @_;

	for my $n ($set->newer) {
		$set->find_kept_handle($n, $state);
	}
}

sub precomplete_handle
{
	my ($set, $n, $state) = @_;
	unless (defined $n->{location} && defined $n->{location}{update_info}) {
		$n->complete($state);
	}
}

sub precomplete
{
	my ($set, $state) = @_;

	for my $n ($set->newer) {
		$set->precomplete_handle($n, $state);
	}
}

sub complete
{
	my ($set, $state) = @_;

	for my $n ($set->newer) {
		$n->complete($state);
		my $plist = $n->plist;
		return 1 if !defined $plist;
		return 1 if $n->has_error;
	}
	# XXX kept must have complete plists to be able to track 
	# libs for OldLibs
	for my $o ($set->older, $set->kept) {
		$o->complete_old($state);
	}

	my $check = $set->install_issues($state);
	return 0 if !defined $check;

	if ($check) {
		$state->{bad}++;
		$set->cleanup(OpenBSD::Handle::CANT_INSTALL, $check);
		$state->tracker->cant($set);
	}
	return 1;
}

sub find_conflicts
{
	my ($set, $state) = @_;

	my $c = $set->conflict_cache;

	for my $handle ($set->newer) {
		$c->add($handle, $state);
	}
	return $c->list;
}

sub mark_as_manual_install
{
	my $set = shift;

	for my $handle ($set->newer) {
		my $plist = $handle->plist;
		$plist->has('manual-installation') or
		    OpenBSD::PackingElement::ManualInstallation->add($plist);
	}
}

sub updates
{
	my ($n, $plist) = @_;
	if (!$n->location->update_info->match_pkgpath($plist)) {
		return 0;
	}
	if (!$n->conflict_list->conflicts_with($plist->pkgname)) {
		return 0;
	}
	my $r = OpenBSD::PackageName->from_string($n->pkgname)->compare(
	    OpenBSD::PackageName->from_string($plist->pkgname));
	if (defined $r && $r < 0) {
		return 0;
	}
	return 1;
}

sub is_an_update_from
{
	my ($set, @conflicts) = @_;
LOOP:	for my $c (@conflicts) {
		next if $c =~ m/^\.libs\d*\-/;
		next if $c =~ m/^partial\-/;
		my $plist = OpenBSD::PackingList->from_installation($c, \&OpenBSD::PackingList::UpdateInfoOnly);
		return 0 unless defined $plist;
		for my $n ($set->newer) {
			if (updates($n, $plist)) {
				next LOOP;
			}
		}
	    	return 0;
	}
	return 1;
}

sub install_issues
{
	my ($set, $state) = @_;

	my @conflicts = $set->find_conflicts($state);

	if (@conflicts == 0) {
		if ($state->defines('update_only')) {
			return "only update, no install";
		} else {
			return 0;
		}
	}

	if (!$state->{allow_replacing}) {
		if (grep { !/^\.libs\d*\-/ && !/^partial\-/ } @conflicts) {
			if (!$set->is_an_update_from(@conflicts)) {
				$state->errsay("Can't install #1 because of conflicts (#2)",
				    $set->print, join(',', @conflicts));
				return "conflicts";
			}
		}
	}

	my $later = 0;
	for my $toreplace (@conflicts) {
		if ($state->tracker->is_installed($toreplace)) {
			$state->errsay("Cannot replace #1 in #2: just got installed",
			    $toreplace, $set->print);
			return "replacing just installed";
		}

		next if defined $set->{older}{$toreplace};
		next if defined $set->{kept}{$toreplace};

		$later = 1;
		my $s = $state->tracker->is_to_update($toreplace);
		if (defined $s && $s ne $set) {
			$set->merge($state->tracker, $s);
		} else {
			my $h = OpenBSD::Handle->create_old($toreplace, $state);
			$set->add_older($h);
		}
	}

	return if $later;


	my $manual_install = 0;

	for my $old ($set->older) {
		my $name = $old->pkgname;

		if ($old->has_error(OpenBSD::Handle::NOT_FOUND)) {
			$state->fatal("can't find #1 in installation", $name);
		}
		if ($old->has_error(OpenBSD::Handle::BAD_PACKAGE)) {
			$state->fatal("couldn't find packing-list for #1", 
			    $name);
		}

		if ($old->plist->has('manual-installation')) {
			$manual_install = 1;
		}
	}

	$set->mark_as_manual_install if $manual_install;

	return 0;
}

sub try_merging
{
	my ($set, $m, $state) = @_;

	my $s = $state->tracker->is_to_update($m);
	if (!defined $s) {
		$s = $state->updateset->add_older(
		    OpenBSD::Handle->create_old($m, $state));
	}
	if ($state->updater->process_set($s, $state)) {
		$state->say("Merging #1 (#2)", $s->print, $state->ntogo);
		$set->merge($state->tracker, $s);
		return 1;
	} else {
		$state->errsay("NOT MERGING: can't find update for #1 (#2)",
		    $s->print, $state->ntogo);
		return 0;
	}
}

sub check_forward_dependencies
{
	my ($set, $state) = @_;

	require OpenBSD::ForwardDependencies;
	$set->{forward} = OpenBSD::ForwardDependencies->find($set);
	my $bad = $set->{forward}->check($state);

	if (%$bad) {
		my $no_merge = 1;
		if (!$state->defines('dontmerge')) {
			my $okay = 1;
			for my $m (keys %$bad) {
				if ($set->{kept}{$m}) {
					$okay = 0;
					next;
				}
				if ($set->try_merging($m, $state)) {
					$no_merge = 0;
				} else {
					$okay = 0;
				}
			}
			return 0 if $okay == 1;
		}
		if ($state->defines('updatedepends')) {
			$state->errsay("Forcing update");
			return $no_merge;
		} elsif ($state->confirm_defaults_to_no(
		    "Proceed with update anyway")) {
				return $no_merge;
		} else {
				return undef;
		}
	}
	return 1;
}

sub recheck_conflicts
{
	my ($set, $state) = @_;

	# no conflicts between newer sets nor kept sets
	for my $h ($set->newer, $set->kept) {
		for my $h2 ($set->newer, $set->kept) {
			next if $h2 == $h;
			if ($h->conflict_list->conflicts_with($h2->pkgname)) {
				$state->errsay("#1: internal conflict between #2 and #3",
				    $set->print, $h->pkgname, $h2->pkgname);
				return 0;
			}
		}
	}

	return 1;
}

package OpenBSD::PkgAdd;
our @ISA = qw(OpenBSD::AddDelete);

use OpenBSD::PackingList;
use OpenBSD::PackageInfo;
use OpenBSD::PackageName;
use OpenBSD::PkgCfl;
use OpenBSD::Add;
use OpenBSD::SharedLibs;
use OpenBSD::UpdateSet;
use OpenBSD::Error;

sub failed_message
{
	my ($base_msg, $received, @l) = @_;
	my $msg = $base_msg;
	if ($received) {
		$msg = "Caught SIG$received. $msg";
	}
	if (@l > 0) {
		$msg.= ", partial installation recorded as ".join(',', @l);
	}
	return $msg;
}

sub save_partial_set
{
	my ($set, $state) = @_;

	return () if $state->{not};
	my @l = ();
	for my $h ($set->newer) {
		next unless defined $h->{partial};
		push(@l, OpenBSD::Add::record_partial_installation($h->plist, $state, $h->{partial}));
	}
	return @l;
}

sub partial_install
{
	my ($base_msg, $set, $state) = @_;
	return failed_message($base_msg, $state->{received}, save_partial_set($set, $state));
}

# quick sub to build the dependency arcs for older packages
# newer packages are handled by Dependencies.pm
sub build_before
{
	my %known = map {($_->pkgname, 1)} @_;
	require OpenBSD::RequiredBy;
	for my $c (@_) {
		for my $d (OpenBSD::RequiredBy->new($c->pkgname)->list) {
			push(@{$c->{before}}, $d) if $known{$d};
		}
	}
}

sub okay
{
	my ($h, $c) = @_;

	for my $d (@{$c->{before}}) {
		return 0 if !$h->{$d};
	}
	return 1;
}

sub iterate
{
	my $sub = pop @_;
	my $done = {};
	my $something_done;

	do {
		$something_done = 0;

		for my $c (@_) {
			next if $done->{$c->pkgname};
			if (okay($done, $c)) {
				&$sub($c);
				$done->{$c->pkgname} = 1;
				$something_done = 1;
			}
		}
	} while ($something_done);
	# if we can't do stuff in order, do it anyway
	for my $c (@_) {
		next if $done->{$c->pkgname};
		&$sub($c);
	}
}

sub delete_old_packages
{
	my ($set, $state) = @_;

	build_before($set->older_to_do);
	iterate($set->older_to_do, sub {
		return if $state->{size_only};
		my $o = shift;
		$set->setup_header($state, $o, "deleting");
		my $oldname = $o->pkgname;
		$state->set_name_from_handle($o, '-');
		require OpenBSD::Delete;
		try {
			OpenBSD::Delete::delete_plist($o->plist, $state);
		} catch {
			$state->errsay($_);
			$state->fatal(partial_install(
			    "Deinstallation of $oldname failed",
			    $set, $state));
		};

		if (defined $state->{updatedepends}) {
			delete $state->{updatedepends}->{$oldname};
		}
		OpenBSD::PkgCfl::unregister($o, $state);
	});
	$set->cleanup_old_shared($state);
	# Here there should be code to handle old libs
}

sub delayed_delete
{
	my $state = shift;
	for my $realname (@{$state->{delayed}}) {
		if (!unlink $realname) {
			$state->errsay("Problem deleting #1: #2", $realname, 
			    $!);
			$state->log("deleting #1 failed: #2", $realname, $!);
		}
	}
	delete $state->{delayed};
}

sub really_add
{
	my ($set, $state) = @_;

	my $errors = 0;

	# XXX in `combined' updates, some dependencies may remove extra
	# packages, so we do a double-take on the list of packages we
	# are actually replacing.
	my $replacing = 0;
	if ($set->older_to_do) {
		$replacing = 1;
	}
	$state->{replacing} = $replacing;

	my $handler = sub {
		$state->{received} = shift;
		$state->errsay("Interrupted");
		if ($state->{hardkill}) {
			delete $state->{hardkill};
			return;
		}
		$state->{interrupted}++;
	};
	local $SIG{'INT'} = $handler;
	local $SIG{'QUIT'} = $handler;
	local $SIG{'HUP'} = $handler;
	local $SIG{'KILL'} = $handler;
	local $SIG{'TERM'} = $handler;

	$state->{hardkill} = $state->{delete_first};

	if ($replacing) {
		require OpenBSD::OldLibs;
		OpenBSD::OldLibs->save($set, $state);
	}

	if ($state->{delete_first}) {
		delete_old_packages($set, $state);
	}

	for my $handle ($set->newer) {
		next if $state->{size_only};
		$set->setup_header($state, $handle, "extracting");

		try {
			OpenBSD::Add::perform_extraction($handle, $state);
		} catch {
			unless ($state->{interrupted}) {
				$state->errsay($_);
				$errors++;
			}
		};
		if ($state->{interrupted} || $errors) {
			$state->fatal(partial_install("Installation of ".
			    $handle->pkgname." failed", $set, $state));
		}
	}
	if ($state->{delete_first}) {
		delayed_delete($state);
	} else {
		$state->{hardkill} = 1;
		delete_old_packages($set, $state);
	}

	iterate($set->newer, sub {
		return if $state->{size_only};
		my $handle = shift;
		my $pkgname = $handle->pkgname;
		my $plist = $handle->plist;

		$set->setup_header($state, $handle, "installing");
		$state->set_name_from_handle($handle, '+');

		try {
			OpenBSD::Add::perform_installation($handle, $state);
		} catch {
			unless ($state->{interrupted}) {
				$state->errsay($_);
				$errors++;
			}
		};

		unlink($plist->infodir.CONTENTS);
		if ($state->{interrupted} || $errors) {
			$state->fatal(partial_install("Installation of $pkgname failed",
			    $set, $state));
		}
	});
	$set->setup_header($state);
	$state->progress->next($state->ntogo(-1));
	for my $handle ($set->newer) {
		my $pkgname = $handle->pkgname;
		my $plist = $handle->plist;
		OpenBSD::SharedLibs::add_libs_from_plist($plist, $state);
		OpenBSD::Add::tweak_plist_status($plist, $state);
		OpenBSD::Add::register_installation($plist, $state);
		add_installed($pkgname);
		delete $handle->{partial};
		OpenBSD::PkgCfl::register($handle, $state);
		if ($set->{quirks}) {
			$handle->location->{repository}->setup_cache($state->{setlist});
		}
	}
	delete $state->{partial};
	$set->{solver}->register_dependencies($state);
	if ($replacing) {
		$set->{forward}->adjust($state);
	}
	if ($state->{repairdependencies}) {
		$set->{solver}->repair_dependencies($state);
	}
	delete $state->{delete_first};
	$state->syslog("Added #1", $set->print);
	if ($state->{received}) {
		die "interrupted";
	}
}

sub newer_has_errors
{
	my ($set, $state) = @_;

	for my $handle ($set->newer) {
		if ($handle->has_error(OpenBSD::Handle::ALREADY_INSTALLED)) {
			$set->cleanup(OpenBSD::Handle::ALREADY_INSTALLED);
			return 1;
		}
		if ($handle->has_error) {
			$state->set_name_from_handle($handle);
			$state->log("Can't install #1: #2",
			    $handle->pkgname, $handle->error_message)
			    unless $handle->has_reported_error;
			$state->{bad}++;
			$set->cleanup($handle->has_error);
			$state->tracker->cant($set);
			return 1;
		}
	}
	return 0;
}

sub newer_is_bad_arch
{
	my ($set, $state) = @_;

	for my $handle ($set->newer) {
		if ($handle->plist->has('arch')) {
			unless ($handle->plist->{arch}->check($state->{arch})) {
				$state->set_name_from_handle($handle);
				$state->log("#1 is not for the right architecture",
				    $handle->pkgname);
				if (!$state->defines('arch')) {
					$state->{bad}++;
					$set->cleanup(OpenBSD::Handle::CANT_INSTALL);
					$state->tracker->cant($set);
					return 1;
				}
			}
		}
	}
	return 0;
}

sub may_tie_files
{
	my ($set, $state) = @_;
	if ($set->newer > 0 && $set->older_to_do > 0 && 
	    !$state->defines('donttie')) {
		my $sha = {};

		for my $o ($set->older_to_do) {
			$set->setup_header($state, $o, "hashing");
			$state->progress->visit_with_count($o->{plist}, 
			    'hash_files', $sha);
		}
		for my $n ($set->newer) {
			$set->setup_header($state, $n, "tieing");
			$state->progress->visit_with_count($n->{plist}, 
			    'tie_files', $sha);
		}
	}
}

sub process_set
{
	my ($self, $set, $state) = @_;

	$state->{current_set} = $set;

	if (!$state->updater->process_set($set, $state)) {
		return ();
	}

	$set->setup_header($state, undef, "processing");
	$state->progress->message("...");
	$set->precomplete($state);
	for my $handle ($set->newer) {
		if ($state->tracker->is_installed($handle->pkgname)) {
			$set->move_kept($handle);
			$handle->{tweaked} = OpenBSD::Add::tweak_package_status($handle->pkgname, $state);
		}
	}

	if (newer_has_errors($set, $state)) {
		return ();
	}

	my @deps = $set->solver->solve_depends($state);
	if ($state->verbose >= 2) {
		$set->solver->dump($state);
	}
	if (@deps > 0) {
		$state->build_deptree($set, @deps);
		$set->solver->check_for_loops($state);
		return (@deps, $set);
	}

	$set->figure_out_kept($state);

	if ($set->newer == 0 && $set->older_to_do == 0) {
		$state->tracker->uptodate($set);
		return ();
	}

	if (!$set->complete($state)) {
		return $set;
	}

	if (newer_has_errors($set, $state)) {
		return ();
	}

	for my $h ($set->newer) {
		$set->check_security($state, $h->plist, $h);
	}

	if (newer_is_bad_arch($set, $state)) {
		return ();
	}

	if ($set->older_to_do) {
		my $r = $set->check_forward_dependencies($state);
		if (!defined $r) {
			$state->{bad}++;
			$set->cleanup(OpenBSD::Handle::CANT_INSTALL);
			$state->tracker->cant($set);
			return ();
		}
		if ($r == 0) {
			return $set;
		}
	}

	# verify dependencies have been installed
	my $baddeps = $set->solver->check_depends;

	if (@$baddeps) {
		$state->errsay("Can't install #1: can't resolve #2",
		    $set->print, join(',', @$baddeps));
		$state->{bad}++;
		$set->cleanup(OpenBSD::Handle::CANT_INSTALL,"bad dependencies");
		$state->tracker->cant($set);
		return ();
	}

	if (!$set->solver->solve_wantlibs($state)) {
		$state->{bad}++;
		$set->cleanup(OpenBSD::Handle::CANT_INSTALL, "libs not found");
		$state->tracker->cant($set);
		return ();
	}
	if (!$set->solver->solve_tags($state)) {
		$set->cleanup(OpenBSD::Handle::CANT_INSTALL, "tags not found");
		$state->tracker->cant($set);
		$state->{bad}++;
		return ();
	}
	if (!$set->recheck_conflicts($state)) {
		$state->{bad}++;
		$set->cleanup(OpenBSD::Handle::CANT_INSTALL, "fatal conflicts");
		$state->tracker->cant($set);
		return ();
	}
	# sets with only tags can be updated without temp files while skipping
	# installing
	if ($set->older_to_do) {
		require OpenBSD::Replace;
		$set->{simple_update} = 
		    OpenBSD::Replace::set_has_no_exec($set, $state);
	} else {
		$set->{simple_update} = 1;
	}
	if ($state->verbose && !$set->{simple_update}) {
		$state->say("Update Set #1 runs exec commands", $set->print);
	}
	if ($set->newer > 0 || $set->older_to_do > 0) {
		if ($state->{not}) {
			$state->status->what("Pretending to add");
		} else {
			$state->status->what("Adding");
		}
		for my $h ($set->newer) {
			$h->plist->set_infodir($h->location->info);
			delete $h->location->{contents};
		}

		may_tie_files($set, $state);
		if (!$set->validate_plists($state)) {
			$state->{bad}++;
			$set->cleanup(OpenBSD::Handle::CANT_INSTALL,
			    "file issues");
			$state->tracker->cant($set);
			return ();
		}

		really_add($set, $state);
	}
	$set->cleanup;
	$state->tracker->done($set);
	if (defined $state->debug_cache_directory) {
		for my $p ($set->newer_names) {
			$self->may_grab_debug_for($p, 0, $state);
		}
	}
	return ();
}

sub may_grab_debug_for
{
	my ($class, $orig, $kept, $state) = @_;
	return if $orig =~ m/^debug\-/;
	my $dbg = "debug-$orig";
	return if $state->tracker->is_known($dbg);
	return if OpenBSD::PackageInfo::is_installed($dbg);
	my $d = $state->debug_cache_directory;
	return if $kept && -f "$d/$dbg.tgz";
	$class->grab_debug_package($d, $dbg, $state);
}

sub grab_debug_package
{
	my ($class, $d, $dbg, $state) = @_;

	my $o = $state->locator->find($dbg);
	return if !defined $o;
	require OpenBSD::Temp;
	my ($fh, $name) = OpenBSD::Temp::permanent_file($d, "debug-pkg");
	if (!defined $fh) {
		$state->errsay(OpenBSD::Temp->last_error);
		return;
	}
	my $r = fork;
	if (!defined $r) {
		$state->fatal("Cannot fork: #1", $!);
	} elsif ($r == 0) {
		$DB::inhibit_exit = 0;
		open(STDOUT, '>&', $fh);
		open(STDERR, '>>', $o->{errors});
		$o->{repository}->grab_object($o);
	} else {
		close($fh);
		waitpid($r, 0);
		my $c = $?;
		$o->{repository}->parse_problems($o->{errors}, 1, $o);
		if ($c == 0) {
			rename($name, "$d/$dbg.tgz");
		} else {
			unlink($name);
			$state->errsay("Grabbing debug package failed: #1",
				$state->child_error($c));
		}
	}
}

sub inform_user_of_problems
{
	my $state = shift;
	my @cantupdate = $state->tracker->cant_list;
	if (@cantupdate > 0) {
		$state->run_quirks(
		    sub {
		    	my $quirks = shift;
			$quirks->filter_obsolete(\@cantupdate, $state);
		    });

		$state->say("Couldn't find updates for #1", 
		    join(' ', sort @cantupdate)) if @cantupdate > 0;
		if (@cantupdate > 0) {
			$state->{bad}++;
		}
	}
	if (defined $state->{issues}) {
		$state->say("There were some ambiguities. ".
		    "Please run in interactive mode again.");
	}
	my @install = $state->tracker->cant_install_list;
	if (@install > 0) {
		$state->say("Couldn't install #1", 
		    join(' ', sort @install));
		$state->{bad}++;
	}
}

# if we already have quirks, we update it. If not, we try to install it.
sub quirk_set
{
	my $state = shift;
	require OpenBSD::Search;

	my $set = $state->updateset;
	$set->{quirks} = 1;
	my $l = $state->repo->installed->match_locations(OpenBSD::Search::Stem->new('quirks'));
	if (@$l > 0) {
		$set->add_older(map {OpenBSD::Handle->from_location($_)} @$l);
	} else {
		$set->add_hints2('quirks');
	}
	return $set;
}

sub do_quirks
{
	my ($self, $state) = @_;
	my $set = quirk_set($state);
	$self->process_set($set, $state);
}


sub process_parameters
{
	my ($self, $state) = @_;
	my $add_hints = $state->{fuzzy} ? "add_hints" : "add_hints2";

	# match against a list
	if ($state->{pkglist}) {
		open my $f, '<', $state->{pkglist} or
		    $state->fatal("bad list #1: #2", $state->{pkglist}, $!);
		while (<$f>) {
			chomp;
			s/\s.*//;
			s/\.tgz$//;
			push(@{$state->{setlist}},
			    $state->updateset->$add_hints($_));
		}
	}

	# update existing stuff
	if ($state->{update}) {

		if (@ARGV == 0) {
			@ARGV = sort(installed_packages());
			$state->{allupdates} = 1;
		}
		my $inst = $state->repo->installed;
		for my $pkgname (@ARGV) {
			my $l;

			next if $pkgname =~ m/^quirks\-\d/;
			if (OpenBSD::PackageName::is_stem($pkgname)) {
				$l = $state->updater->stem2location($inst, $pkgname, $state);
			} else {
				$l = $inst->find($pkgname);
			}
			if (!defined $l) {
				$state->say("Problem finding #1", $pkgname);
			} else {
				push(@{$state->{setlist}},
				    $state->updateset->add_older(OpenBSD::Handle->from_location($l)));
			}
		}
	} else {

	# actual names
		for my $pkgname (@ARGV) {
			next if $pkgname =~ m/^quirks\-\d/;
			push(@{$state->{setlist}},
			    $state->updateset->$add_hints($pkgname));
		}
	}
}

sub finish_display
{
	my ($self, $state) = @_;
	OpenBSD::Add::manpages_index($state);

	# and display delayed thingies.
	if (defined $state->{updatedepends} && %{$state->{updatedepends}}) {
		$state->say("Forced updates, bogus dependencies for ",
		    join(' ', sort(keys %{$state->{updatedepends}})),
		    " may remain");
	}
	inform_user_of_problems($state);
}

sub tweak_list
{
	my ($self, $state) = @_;

	$state->run_quirks(
	    sub {
	    	my $quirks = shift;
		$quirks->tweak_list($state->{setlist}, $state);
	    });
}

sub main
{
	my ($self, $state) = @_;

	$state->progress->set_header('');
	$self->do_quirks($state);

	$self->process_setlist($state);
}


sub new_state
{
	my ($self, $cmd) = @_;
	return OpenBSD::PkgAdd::State->new($cmd);
}

1;
