#!/usr/perl5/bin/perl -w
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License, Version 1.0 only
# (the "License").  You may not use this file except in compliance
# with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
#ident	"%Z%%M%	%I%	%E% SMI"
#

require 5.005;
use strict;
use locale;
use Errno;
use Fcntl;
use File::Basename;
use Getopt::Std;
use Getopt::Long qw(:config no_ignore_case bundling);
use POSIX qw(locale_h);
use Sun::Solaris::Utils qw(textdomain gettext);
use Sun::Solaris::Project qw(:ALL :PRIVATE);

#
# Print a usage message and exit.
#
sub usage
{
	my (@msg) = @_;
	my $prog = basename($0);
	my $space = ' ' x length($prog);
	print(STDERR "$prog: @msg\n") if (@msg);
	printf(STDERR gettext(
	    "Usage: %s [-n] [-f filename]\n"), $prog);
	printf(STDERR gettext(
	    "       %s [-n] [-f filename] [-p projid [-o]] [-c comment]\n".
            "       %s [-a|-s|-r] [-U user[,user...]] [-G group[,group...]]\n".
            "       %s [-K name[=value[,value...]]] [-l new_projectname] ".
	    "project\n"), $prog, $space, $space, $space);
	exit(2);
}

#
# Print a list of error messages and exit.
#
sub error
{
	my $exit = $_[0][0];
	my $prog = basename($0) . ': ';
	foreach my $err (@_) {
		my ($e, $fmt, @args) = @$err;
		printf(STDERR $prog . $fmt . "\n", @args);
	}
	exit($exit);
}

#
# Merge an array of users/groups with an existing array.  The array to merge
# is the first argument, an array ref is the second argument.  The third
# argument is the mode which can be one of:
#     add	add all entries in the first arg to the second
#     remove	remove all entries in the first arg from the second
#     replace	replace the second arg by the first
# The resulting array is returned as a reference.
#
sub merge_lists
{
	my ($new, $old, $mode) = @_;
	my @err;

	if ($mode eq 'add') {
		my @merged = @$old;
		my %look = map { $_ => 1 } @$old;
		my @leftover;
		foreach my $e (@$new) {
			if (! exists($look{$e})) {
				push(@merged, $e);
			} else {
				push(@leftover, $e);
			}
		}
		if (@leftover) {
			push(@err,
			    [6, gettext('Project already contains "%s"'),
			    join(',', @leftover)]);
			return (1, \@err);
		}

		return(0, \@merged);

	} elsif ($mode eq 'remove') {

		my %seen;
		my @dups = grep($seen{$_}++ == 1, @$new);
		if (@dups) {
			push(@err, [6, gettext('Duplicate names "%s"'),
			    join(',', @dups)]);
			return (1, \@err);
		}
		my @merged;
		my %look = map { $_ => 0 } @$new;
		foreach my $e (@$old) {
			if (exists($look{$e})) {
				$look{$e}++;
			} else {
				push(@merged, $e);
			}
		}
		my @leftover = grep(! $look{$_}, keys(%look));
		if (@leftover) {
			push(@err, [6,
		            gettext('Project does not contain "%s"'),
			    join(',', @leftover)]);
			return (1, \@err);
		}
		return (0, \@merged);

	} elsif ($mode eq 'replace' || $mode eq 'substitute') {
		return (0, $new);
	}
}

#
# merge_values(ref to listA, ref to listB, mode
#
# Merges the values in listB with the values in listA.  Dups are not
# merged away, but instead are maintained.
#
# modes:
#	add   :	add values in listB to listA
#	remove:	removes first instance of each value in listB from listA
#	
sub merge_values
{

	my ($new, $old, $mode) = @_;
	my $undefined;
	my @merged;
	my $lastmerged;
	my ($oldval, $newval);
	my $found;
	my @err;

	if (!defined($old) && !defined($new)) {
		return (0, $undefined);
	}
					     
	if ($mode eq 'add') {

		if (defined($old)) {
			push(@merged, @$old);
		}
		if (defined($new)) {
			push(@merged, @$new);
		}
		return (0, \@merged);

	} elsif ($mode eq 'remove') {

		$lastmerged = $old;
		foreach $newval (@$new) {
			$found = 0;
			@merged = ();
			foreach $oldval (@$lastmerged) {
				if (!$found &&
				    projent_values_equal($newval, $oldval)) {
					$found = 1;
				} else {
					push(@merged, $oldval);
				}

			}
			if (!$found) {
				push(@err, [6, gettext(
				    'Value "%s" not found'),
				    projent_values2string($newval)]);
			}
			@$lastmerged = @merged;
		}

		if (@err) {
			return (1, \@err);
		} else {
			return (0, \@merged);
		}
	}
}

#
# merge_attribs(listA ref, listB ref, mode)
#
# Merge listB of attribute/values hash refs with listA
# Each hash ref should have keys "name" and "values"
#
# modes:
#     add	For each attribute in listB, add its values to
#	        the matching attribute in listA.  If listA does not
#		contain this attribute, add it.
#
#     remove	For each attribute in listB, remove its values from
#	        the matching attribute in listA.  If all of an
#		attributes values are removed, the attribute is removed.
#		If the attribute in listB has no values, then the attribute
#		and all of it's values are removed from listA
#
#     substitute For each attribute in listB, replace the values of
#	        the matching attribute in listA with its values.  If
#		listA does not contain this attribute, add it.
#
#     replace	Return listB
#
# The resulting array is returned as a reference.
#
sub merge_attribs
{
	my ($new, $old, $mode) = @_;
	my @merged;
	my @err;
	my $ret;
	my $tmp;
	my $newattrib;
	my $oldattrib;
	my $values;
       
	if ($mode eq 'add') {

		my %oldhash;
		push(@merged, @$old);
		%oldhash = map { $_->{'name'} => $_ } @$old;
		foreach $newattrib (@$new) {

			$oldattrib = $oldhash{$newattrib->{'name'}};
			if (defined($oldattrib)) {
				($ret, $tmp) = merge_values(
				    $newattrib->{'values'},
				    $oldattrib->{'values'},
				    $mode);

				if ($ret != 0) {
					push(@err, @$tmp);
				} else {
					$oldattrib->{'values'} = $tmp;
				}
			} else {
				push(@merged, $newattrib);
			}
		}
		if (@err) {
			return (1, \@err);
		} else {
			return (0, \@merged);
		}

	} elsif ($mode eq 'remove') {

		my %seen;
		my @dups = grep($seen{$_}++ == 1, map { $_->{'name'} } @$new);
		if (@dups) {
			push(@err, [6, gettext(
			    'Duplicate Attributes "%s"'),
			     join(',', @dups)]);
			return (1, \@err);
		}
		my %toremove = map { $_->{'name'} => $_ } @$new;

		foreach $oldattrib (@$old) {
			$newattrib = $toremove{$oldattrib->{'name'}};
			if (!defined($newattrib)) {

				push(@merged, $oldattrib);
				
			} else {
				if (defined($newattrib->{'values'})) {
					($ret, $tmp) = merge_values(
					    $newattrib->{'values'},
					    $oldattrib->{'values'},
					    $mode);

					if ($ret != 0) {
						push(@err, @$tmp);
					} else {
						$oldattrib->{'values'} = $tmp;
					}
					if (defined($tmp) && @$tmp) {
						push(@merged, $oldattrib);
					}
				}
				delete $toremove{$oldattrib->{'name'}};
			}
		}
		foreach $tmp (keys(%toremove)) {
			push(@err, [6,
		            gettext('Project does not contain "%s"'),
			    $tmp]);
		}

		if (@err) {
			return (1, \@err);
		} else {	
			return (0, \@merged);
		}	

	} elsif ($mode eq 'substitute') {

		my %oldhash;
		push(@merged, @$old);
		%oldhash = map { $_->{'name'} => $_ } @$old;
		foreach $newattrib (@$new) {

			$oldattrib = $oldhash{$newattrib->{'name'}};
			if (defined($oldattrib)) {

				$oldattrib->{'values'} =
				    $newattrib->{'values'};

			} else {
				push(@merged, $newattrib);
			}
		}
		if (@err) {
			return (1, \@err);
		} else {
			return (0, \@merged);
		}
	
	} elsif ($mode eq 'replace') {
		return (0, $new);
	}
}

#
# Main routine of script.
#
# Set the message locale.
#
setlocale(LC_ALL, '');
textdomain(TEXT_DOMAIN);


# Process command options and do some initial command-line validity checking.
my ($pname, $flags);
$flags = {};
my $modify = 0;

my $projfile = &PROJF_PATH;
my $opt_n;
my $opt_c;
my $opt_o;
my $opt_p;
my $opt_l;
my $opt_a;
my $opt_r;
my $opt_s;
my $opt_U;
my $opt_G;
my @opt_K;

GetOptions("f=s" => \$projfile,
	   "n"   => \$opt_n,
	   "c=s" => \$opt_c,
	   "o"	 => \$opt_o,
	   "p=s" => \$opt_p,
	   "l=s" => \$opt_l,
	   "s"	 => \$opt_s,
	   "r"	 => \$opt_r,
	   "a"	 => \$opt_a,
	   "U=s" => \$opt_U,
	   "G=s" => \$opt_G,
	   "K=s" => \@opt_K) || usage();

usage(gettext('Invalid command-line arguments')) if (@ARGV > 1);

if ($opt_c || $opt_G || $opt_l || $opt_p || $opt_U || @opt_K) {
	$modify = 1;
	if (! defined($ARGV[0])) {
		usage(gettext('No project name specified'));
	}
}

if (!$modify && defined($ARGV[0])) {
	usage(gettext('missing -c, -G, -l, -p, -U, or -K'));
}

if ($modify && $projfile eq '-') {
	usage(gettext('Cannot modify standard input'));
}

$pname = $ARGV[0];
usage(gettext('-o requires -p projid to be specified'))
    if (defined($opt_o) && ! defined($opt_p));
usage(gettext('-a, -r, and -s are mutually exclusive'))
    if ((defined($opt_a) && (defined($opt_r) || defined($opt_s))) ||
	(defined($opt_r) && (defined($opt_a) || defined($opt_s))) ||
	(defined($opt_s) && (defined($opt_a) || defined($opt_r))));

usage(gettext('-a and -r require -U users or -G groups to be specified'))
    if ((defined($opt_a) || defined($opt_r) || defined($opt_s)) &&
    ! (defined($opt_U) || defined($opt_G) || (@opt_K)));


if (defined($opt_a)) {
	$flags->{mode} = 'add';
} elsif (defined($opt_r)) {
	$flags->{mode} = 'remove';
} elsif (defined($opt_s)) {
	$flags->{mode} = 'substitute';
} else {
	$flags->{mode} = 'replace';
}

# Fabricate an unique temporary filename.
my $tmpprojf = $projfile . ".tmp.$$";

my $pfh;

#
# Read the project file.  sysopen() is used so we can control the file mode.
# Handle special case for standard input.
if ($projfile eq '-') {
	open($pfh, "<&=STDIN") or error( [10,
	    gettext('Cannot open standard input')]);
} elsif (! sysopen($pfh, $projfile, O_RDONLY)) {
	error([10, gettext('Cannot open %s: %s'), $projfile, $!]);
}
my ($mode, $uid, $gid) = (stat($pfh))[2,4,5];


if ($opt_n) {
	$flags->{'validate'} = 'false';
} else {
	$flags->{'validate'} = 'true';
}

$flags->{'res'} = 'true';
$flags->{'dup'} = 'true';

my ($ret, $pf) = projf_read($pfh, $flags);
if ($ret != 0) {
	error(@$pf);
}
close($pfh);
my $err;
my $tmperr;
my $value;

# Find existing record.
my ($proj, $idx);
$idx = 0;

if (defined($pname)) {
	foreach my $r (@$pf) {	
		if ($r->{'name'} eq $pname) {
			$proj = $r;
			last;
		}	
		$idx++;
	}
	error([6, gettext('Project "%s" does not exist'), $pname])
	    if (! $proj);
}
#
# If there are no modification options, simply reading the file, which
# includes parsing and verifying, is sufficient.
#
if (!$modify) {
	exit(0);
}

foreach my $r (@$pf) {
	if ($r->{'name'} eq $pname) {
		$proj = $r;
		last;
	}	
	$idx++;
}

# Update the record as appropriate.
$err = [];

# Set new project name.
if (defined($opt_l)) {

	($ret, $value) = projent_parse_name($opt_l);
	if ($ret != 0) {
		push(@$err, @$value);
	} else {
		$proj->{'name'} = $value;
		if (!defined($opt_n)) {
			($ret, $tmperr) =
			    projent_validate_unique_name($proj, $pf);
			if ($ret != 0) {
				push(@$err, @$tmperr);
			}
		}
	}
}

# Set new project id.
if (defined($opt_p)) {

	($ret, $value) = projent_parse_projid($opt_p);
	if ($ret != 0) {
		push(@$err, @$value);
	} else {
		$proj->{'projid'} = $value;

		# Check for dupicate.
		if ((!defined($opt_n)) && (!defined($opt_o))) {
			($ret, $tmperr) =
			    projent_validate_unique_id($proj, $pf);
			if ($ret != 0) {
				push(@$err, @$tmperr);
			}
		}
	}	
}

# Set new comment.
if (defined($opt_c)) {

	($ret, $value) = projent_parse_comment($opt_c);
	if ($ret != 0) {
		push(@$err, @$value);
	} else {
		$proj->{'comment'} = $value;
	}
}

# Set new users.
if (defined($opt_U)) {

	my @sortlist;
	my $list;
	($ret, $list) = projent_parse_users($opt_U, {'allowspaces' => 1});
	if ($ret != 0) {
		push(@$err, @$list);
	} else {
		($ret, $list) =
		    merge_lists($list, $proj->{'userlist'}, $flags->{mode});
		if ($ret != 0) {
			push(@$err, @$list);
		} else {
			@sortlist = sort(@$list);
			$proj->{'userlist'} = \@sortlist;
		}
	}	
}

# Set new groups.
if (defined($opt_G)) {

	my @sortlist;
	my $list;
	($ret, $list) = projent_parse_groups($opt_G, {'allowspaces' => 1});
	if ($ret != 0) {
		push(@$err, @$list);
	} else {
		($ret, $list) =
		    merge_lists($list, $proj->{'grouplist'}, $flags->{mode});
		if ($ret != 0) {
			push(@$err, @$list);
		} else {
			@sortlist = sort(@$list);
			$proj->{'grouplist'} = \@sortlist;
		}
	}
}

# Set new attributes.
my $attrib;
my @attriblist;

foreach $attrib (@opt_K) {

	my $list;
	($ret, $list) = projent_parse_attributes($attrib, {'allowunits' => 1});
	if ($ret != 0) {
		push(@$err, @$list);
	} else {
		push(@attriblist, @$list);
	}
}

if (@attriblist) {
	my @sortlist;
	my $list;

	($ret, $list) =
	    merge_attribs(\@attriblist, $proj->{'attributelist'},
	    $flags->{mode});
	if ($ret != 0) {
		push(@$err, @$list);
	} else {
		@sortlist =
		    sort { $a->{'name'} cmp $b->{'name'} } @$list;
		$proj->{'attributelist'} = \@sortlist;
	}
}

# Validate all projent fields.
if (!defined($opt_n)) {
	($ret, $tmperr) = projent_validate($proj, $flags);
	if ($ret != 0) {
		push(@$err, @$tmperr);
	}
}
if (@$err) {
	error(@$err);
}

# Write out the project file.
if ($modify) {

	#
	# Mark projent to write based on new values instead of
	# original line.
	#
	$proj->{'modified'} = 'true';
	umask(0000);
	sysopen($pfh, $tmpprojf, O_WRONLY | O_CREAT | O_EXCL, $mode) ||
	    error([10, gettext('Cannot create %s: %s'), $tmpprojf, $!]);
	projf_write($pfh, $pf);
	close($pfh);

	# Update file attributes.
	if (!chown($uid, $gid, $tmpprojf)) {
		unlink($tmpprojf);
		error([10, gettext('Cannot set ownership of %s: %s'),
		    $tmpprojf, $!]);
	}
	if (! rename($tmpprojf, $projfile)) {
		unlink($tmpprojf);
		error([10, gettext('cannot rename %s to %s: %s'),
	            $tmpprojf, $projfile, $!]);
	}

}

exit(0);




