#!/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);