1#!/usr/perl5/bin/perl -w 2# 3# CDDL HEADER START 4# 5# The contents of this file are subject to the terms of the 6# Common Development and Distribution License, Version 1.0 only 7# (the "License"). You may not use this file except in compliance 8# with the License. 9# 10# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 11# or http://www.opensolaris.org/os/licensing. 12# See the License for the specific language governing permissions 13# and limitations under the License. 14# 15# When distributing Covered Code, include this CDDL HEADER in each 16# file and include the License file at usr/src/OPENSOLARIS.LICENSE. 17# If applicable, add the following below this CDDL HEADER, with the 18# fields enclosed by brackets "[]" replaced with your own identifying 19# information: Portions Copyright [yyyy] [name of copyright owner] 20# 21# CDDL HEADER END 22# 23# 24# Copyright 2005 Sun Microsystems, Inc. All rights reserved. 25# Use is subject to license terms. 26# 27#ident "%Z%%M% %I% %E% SMI" 28# 29 30require 5.005; 31use strict; 32use locale; 33use Errno; 34use Fcntl; 35use File::Basename; 36use Getopt::Std; 37use Getopt::Long qw(:config no_ignore_case bundling); 38use POSIX qw(locale_h); 39use Sun::Solaris::Utils qw(textdomain gettext); 40use Sun::Solaris::Project qw(:ALL :PRIVATE); 41 42# 43# Print a usage message and exit. 44# 45sub usage 46{ 47 my (@msg) = @_; 48 my $prog = basename($0); 49 my $space = ' ' x length($prog); 50 print(STDERR "$prog: @msg\n") if (@msg); 51 printf(STDERR gettext( 52 "Usage: %s [-n] [-f filename]\n"), $prog); 53 printf(STDERR gettext( 54 " %s [-n] [-f filename] [-p projid [-o]] [-c comment]\n". 55 " %s [-a|-s|-r] [-U user[,user...]] [-G group[,group...]]\n". 56 " %s [-K name[=value[,value...]]] [-l new_projectname] ". 57 "project\n"), $prog, $space, $space, $space); 58 exit(2); 59} 60 61# 62# Print a list of error messages and exit. 63# 64sub error 65{ 66 my $exit = $_[0][0]; 67 my $prog = basename($0) . ': '; 68 foreach my $err (@_) { 69 my ($e, $fmt, @args) = @$err; 70 printf(STDERR $prog . $fmt . "\n", @args); 71 } 72 exit($exit); 73} 74 75# 76# Merge an array of users/groups with an existing array. The array to merge 77# is the first argument, an array ref is the second argument. The third 78# argument is the mode which can be one of: 79# add add all entries in the first arg to the second 80# remove remove all entries in the first arg from the second 81# replace replace the second arg by the first 82# The resulting array is returned as a reference. 83# 84sub merge_lists 85{ 86 my ($new, $old, $mode) = @_; 87 my @err; 88 89 if ($mode eq 'add') { 90 my @merged = @$old; 91 my %look = map { $_ => 1 } @$old; 92 my @leftover; 93 foreach my $e (@$new) { 94 if (! exists($look{$e})) { 95 push(@merged, $e); 96 } else { 97 push(@leftover, $e); 98 } 99 } 100 if (@leftover) { 101 push(@err, 102 [6, gettext('Project already contains "%s"'), 103 join(',', @leftover)]); 104 return (1, \@err); 105 } 106 107 return(0, \@merged); 108 109 } elsif ($mode eq 'remove') { 110 111 my %seen; 112 my @dups = grep($seen{$_}++ == 1, @$new); 113 if (@dups) { 114 push(@err, [6, gettext('Duplicate names "%s"'), 115 join(',', @dups)]); 116 return (1, \@err); 117 } 118 my @merged; 119 my %look = map { $_ => 0 } @$new; 120 foreach my $e (@$old) { 121 if (exists($look{$e})) { 122 $look{$e}++; 123 } else { 124 push(@merged, $e); 125 } 126 } 127 my @leftover = grep(! $look{$_}, keys(%look)); 128 if (@leftover) { 129 push(@err, [6, 130 gettext('Project does not contain "%s"'), 131 join(',', @leftover)]); 132 return (1, \@err); 133 } 134 return (0, \@merged); 135 136 } elsif ($mode eq 'replace' || $mode eq 'substitute') { 137 return (0, $new); 138 } 139} 140 141# 142# merge_values(ref to listA, ref to listB, mode 143# 144# Merges the values in listB with the values in listA. Dups are not 145# merged away, but instead are maintained. 146# 147# modes: 148# add : add values in listB to listA 149# remove: removes first instance of each value in listB from listA 150# 151sub merge_values 152{ 153 154 my ($new, $old, $mode) = @_; 155 my $undefined; 156 my @merged; 157 my $lastmerged; 158 my ($oldval, $newval); 159 my $found; 160 my @err; 161 162 if (!defined($old) && !defined($new)) { 163 return (0, $undefined); 164 } 165 166 if ($mode eq 'add') { 167 168 if (defined($old)) { 169 push(@merged, @$old); 170 } 171 if (defined($new)) { 172 push(@merged, @$new); 173 } 174 return (0, \@merged); 175 176 } elsif ($mode eq 'remove') { 177 178 $lastmerged = $old; 179 foreach $newval (@$new) { 180 $found = 0; 181 @merged = (); 182 foreach $oldval (@$lastmerged) { 183 if (!$found && 184 projent_values_equal($newval, $oldval)) { 185 $found = 1; 186 } else { 187 push(@merged, $oldval); 188 } 189 190 } 191 if (!$found) { 192 push(@err, [6, gettext( 193 'Value "%s" not found'), 194 projent_values2string($newval)]); 195 } 196 @$lastmerged = @merged; 197 } 198 199 if (@err) { 200 return (1, \@err); 201 } else { 202 return (0, \@merged); 203 } 204 } 205} 206 207# 208# merge_attribs(listA ref, listB ref, mode) 209# 210# Merge listB of attribute/values hash refs with listA 211# Each hash ref should have keys "name" and "values" 212# 213# modes: 214# add For each attribute in listB, add its values to 215# the matching attribute in listA. If listA does not 216# contain this attribute, add it. 217# 218# remove For each attribute in listB, remove its values from 219# the matching attribute in listA. If all of an 220# attributes values are removed, the attribute is removed. 221# If the attribute in listB has no values, then the attribute 222# and all of it's values are removed from listA 223# 224# substitute For each attribute in listB, replace the values of 225# the matching attribute in listA with its values. If 226# listA does not contain this attribute, add it. 227# 228# replace Return listB 229# 230# The resulting array is returned as a reference. 231# 232sub merge_attribs 233{ 234 my ($new, $old, $mode) = @_; 235 my @merged; 236 my @err; 237 my $ret; 238 my $tmp; 239 my $newattrib; 240 my $oldattrib; 241 my $values; 242 243 if ($mode eq 'add') { 244 245 my %oldhash; 246 push(@merged, @$old); 247 %oldhash = map { $_->{'name'} => $_ } @$old; 248 foreach $newattrib (@$new) { 249 250 $oldattrib = $oldhash{$newattrib->{'name'}}; 251 if (defined($oldattrib)) { 252 ($ret, $tmp) = merge_values( 253 $newattrib->{'values'}, 254 $oldattrib->{'values'}, 255 $mode); 256 257 if ($ret != 0) { 258 push(@err, @$tmp); 259 } else { 260 $oldattrib->{'values'} = $tmp; 261 } 262 } else { 263 push(@merged, $newattrib); 264 } 265 } 266 if (@err) { 267 return (1, \@err); 268 } else { 269 return (0, \@merged); 270 } 271 272 } elsif ($mode eq 'remove') { 273 274 my %seen; 275 my @dups = grep($seen{$_}++ == 1, map { $_->{'name'} } @$new); 276 if (@dups) { 277 push(@err, [6, gettext( 278 'Duplicate Attributes "%s"'), 279 join(',', @dups)]); 280 return (1, \@err); 281 } 282 my %toremove = map { $_->{'name'} => $_ } @$new; 283 284 foreach $oldattrib (@$old) { 285 $newattrib = $toremove{$oldattrib->{'name'}}; 286 if (!defined($newattrib)) { 287 288 push(@merged, $oldattrib); 289 290 } else { 291 if (defined($newattrib->{'values'})) { 292 ($ret, $tmp) = merge_values( 293 $newattrib->{'values'}, 294 $oldattrib->{'values'}, 295 $mode); 296 297 if ($ret != 0) { 298 push(@err, @$tmp); 299 } else { 300 $oldattrib->{'values'} = $tmp; 301 } 302 if (defined($tmp) && @$tmp) { 303 push(@merged, $oldattrib); 304 } 305 } 306 delete $toremove{$oldattrib->{'name'}}; 307 } 308 } 309 foreach $tmp (keys(%toremove)) { 310 push(@err, [6, 311 gettext('Project does not contain "%s"'), 312 $tmp]); 313 } 314 315 if (@err) { 316 return (1, \@err); 317 } else { 318 return (0, \@merged); 319 } 320 321 } elsif ($mode eq 'substitute') { 322 323 my %oldhash; 324 push(@merged, @$old); 325 %oldhash = map { $_->{'name'} => $_ } @$old; 326 foreach $newattrib (@$new) { 327 328 $oldattrib = $oldhash{$newattrib->{'name'}}; 329 if (defined($oldattrib)) { 330 331 $oldattrib->{'values'} = 332 $newattrib->{'values'}; 333 334 } else { 335 push(@merged, $newattrib); 336 } 337 } 338 if (@err) { 339 return (1, \@err); 340 } else { 341 return (0, \@merged); 342 } 343 344 } elsif ($mode eq 'replace') { 345 return (0, $new); 346 } 347} 348 349# 350# Main routine of script. 351# 352# Set the message locale. 353# 354setlocale(LC_ALL, ''); 355textdomain(TEXT_DOMAIN); 356 357 358# Process command options and do some initial command-line validity checking. 359my ($pname, $flags); 360$flags = {}; 361my $modify = 0; 362 363my $projfile = &PROJF_PATH; 364my $opt_n; 365my $opt_c; 366my $opt_o; 367my $opt_p; 368my $opt_l; 369my $opt_a; 370my $opt_r; 371my $opt_s; 372my $opt_U; 373my $opt_G; 374my @opt_K; 375 376GetOptions("f=s" => \$projfile, 377 "n" => \$opt_n, 378 "c=s" => \$opt_c, 379 "o" => \$opt_o, 380 "p=s" => \$opt_p, 381 "l=s" => \$opt_l, 382 "s" => \$opt_s, 383 "r" => \$opt_r, 384 "a" => \$opt_a, 385 "U=s" => \$opt_U, 386 "G=s" => \$opt_G, 387 "K=s" => \@opt_K) || usage(); 388 389usage(gettext('Invalid command-line arguments')) if (@ARGV > 1); 390 391if ($opt_c || $opt_G || $opt_l || $opt_p || $opt_U || @opt_K) { 392 $modify = 1; 393 if (! defined($ARGV[0])) { 394 usage(gettext('No project name specified')); 395 } 396} 397 398if (!$modify && defined($ARGV[0])) { 399 usage(gettext('missing -c, -G, -l, -p, -U, or -K')); 400} 401 402if ($modify && $projfile eq '-') { 403 usage(gettext('Cannot modify standard input')); 404} 405 406$pname = $ARGV[0]; 407usage(gettext('-o requires -p projid to be specified')) 408 if (defined($opt_o) && ! defined($opt_p)); 409usage(gettext('-a, -r, and -s are mutually exclusive')) 410 if ((defined($opt_a) && (defined($opt_r) || defined($opt_s))) || 411 (defined($opt_r) && (defined($opt_a) || defined($opt_s))) || 412 (defined($opt_s) && (defined($opt_a) || defined($opt_r)))); 413 414usage(gettext('-a and -r require -U users or -G groups to be specified')) 415 if ((defined($opt_a) || defined($opt_r) || defined($opt_s)) && 416 ! (defined($opt_U) || defined($opt_G) || (@opt_K))); 417 418 419if (defined($opt_a)) { 420 $flags->{mode} = 'add'; 421} elsif (defined($opt_r)) { 422 $flags->{mode} = 'remove'; 423} elsif (defined($opt_s)) { 424 $flags->{mode} = 'substitute'; 425} else { 426 $flags->{mode} = 'replace'; 427} 428 429# Fabricate an unique temporary filename. 430my $tmpprojf = $projfile . ".tmp.$$"; 431 432my $pfh; 433 434# 435# Read the project file. sysopen() is used so we can control the file mode. 436# Handle special case for standard input. 437if ($projfile eq '-') { 438 open($pfh, "<&=STDIN") or error( [10, 439 gettext('Cannot open standard input')]); 440} elsif (! sysopen($pfh, $projfile, O_RDONLY)) { 441 error([10, gettext('Cannot open %s: %s'), $projfile, $!]); 442} 443my ($mode, $uid, $gid) = (stat($pfh))[2,4,5]; 444 445 446if ($opt_n) { 447 $flags->{'validate'} = 'false'; 448} else { 449 $flags->{'validate'} = 'true'; 450} 451 452$flags->{'res'} = 'true'; 453$flags->{'dup'} = 'true'; 454 455my ($ret, $pf) = projf_read($pfh, $flags); 456if ($ret != 0) { 457 error(@$pf); 458} 459close($pfh); 460my $err; 461my $tmperr; 462my $value; 463 464# Find existing record. 465my ($proj, $idx); 466$idx = 0; 467 468if (defined($pname)) { 469 foreach my $r (@$pf) { 470 if ($r->{'name'} eq $pname) { 471 $proj = $r; 472 last; 473 } 474 $idx++; 475 } 476 error([6, gettext('Project "%s" does not exist'), $pname]) 477 if (! $proj); 478} 479# 480# If there are no modification options, simply reading the file, which 481# includes parsing and verifying, is sufficient. 482# 483if (!$modify) { 484 exit(0); 485} 486 487foreach my $r (@$pf) { 488 if ($r->{'name'} eq $pname) { 489 $proj = $r; 490 last; 491 } 492 $idx++; 493} 494 495# Update the record as appropriate. 496$err = []; 497 498# Set new project name. 499if (defined($opt_l)) { 500 501 ($ret, $value) = projent_parse_name($opt_l); 502 if ($ret != 0) { 503 push(@$err, @$value); 504 } else { 505 $proj->{'name'} = $value; 506 if (!defined($opt_n)) { 507 ($ret, $tmperr) = 508 projent_validate_unique_name($proj, $pf); 509 if ($ret != 0) { 510 push(@$err, @$tmperr); 511 } 512 } 513 } 514} 515 516# Set new project id. 517if (defined($opt_p)) { 518 519 ($ret, $value) = projent_parse_projid($opt_p); 520 if ($ret != 0) { 521 push(@$err, @$value); 522 } else { 523 $proj->{'projid'} = $value; 524 525 # Check for dupicate. 526 if ((!defined($opt_n)) && (!defined($opt_o))) { 527 ($ret, $tmperr) = 528 projent_validate_unique_id($proj, $pf); 529 if ($ret != 0) { 530 push(@$err, @$tmperr); 531 } 532 } 533 } 534} 535 536# Set new comment. 537if (defined($opt_c)) { 538 539 ($ret, $value) = projent_parse_comment($opt_c); 540 if ($ret != 0) { 541 push(@$err, @$value); 542 } else { 543 $proj->{'comment'} = $value; 544 } 545} 546 547# Set new users. 548if (defined($opt_U)) { 549 550 my @sortlist; 551 my $list; 552 ($ret, $list) = projent_parse_users($opt_U, {'allowspaces' => 1}); 553 if ($ret != 0) { 554 push(@$err, @$list); 555 } else { 556 ($ret, $list) = 557 merge_lists($list, $proj->{'userlist'}, $flags->{mode}); 558 if ($ret != 0) { 559 push(@$err, @$list); 560 } else { 561 @sortlist = sort(@$list); 562 $proj->{'userlist'} = \@sortlist; 563 } 564 } 565} 566 567# Set new groups. 568if (defined($opt_G)) { 569 570 my @sortlist; 571 my $list; 572 ($ret, $list) = projent_parse_groups($opt_G, {'allowspaces' => 1}); 573 if ($ret != 0) { 574 push(@$err, @$list); 575 } else { 576 ($ret, $list) = 577 merge_lists($list, $proj->{'grouplist'}, $flags->{mode}); 578 if ($ret != 0) { 579 push(@$err, @$list); 580 } else { 581 @sortlist = sort(@$list); 582 $proj->{'grouplist'} = \@sortlist; 583 } 584 } 585} 586 587# Set new attributes. 588my $attrib; 589my @attriblist; 590 591foreach $attrib (@opt_K) { 592 593 my $list; 594 ($ret, $list) = projent_parse_attributes($attrib, {'allowunits' => 1}); 595 if ($ret != 0) { 596 push(@$err, @$list); 597 } else { 598 push(@attriblist, @$list); 599 } 600} 601 602if (@attriblist) { 603 my @sortlist; 604 my $list; 605 606 ($ret, $list) = 607 merge_attribs(\@attriblist, $proj->{'attributelist'}, 608 $flags->{mode}); 609 if ($ret != 0) { 610 push(@$err, @$list); 611 } else { 612 @sortlist = 613 sort { $a->{'name'} cmp $b->{'name'} } @$list; 614 $proj->{'attributelist'} = \@sortlist; 615 } 616} 617 618# Validate all projent fields. 619if (!defined($opt_n)) { 620 ($ret, $tmperr) = projent_validate($proj, $flags); 621 if ($ret != 0) { 622 push(@$err, @$tmperr); 623 } 624} 625if (@$err) { 626 error(@$err); 627} 628 629# Write out the project file. 630if ($modify) { 631 632 # 633 # Mark projent to write based on new values instead of 634 # original line. 635 # 636 $proj->{'modified'} = 'true'; 637 umask(0000); 638 sysopen($pfh, $tmpprojf, O_WRONLY | O_CREAT | O_EXCL, $mode) || 639 error([10, gettext('Cannot create %s: %s'), $tmpprojf, $!]); 640 projf_write($pfh, $pf); 641 close($pfh); 642 643 # Update file attributes. 644 if (!chown($uid, $gid, $tmpprojf)) { 645 unlink($tmpprojf); 646 error([10, gettext('Cannot set ownership of %s: %s'), 647 $tmpprojf, $!]); 648 } 649 if (! rename($tmpprojf, $projfile)) { 650 unlink($tmpprojf); 651 error([10, gettext('cannot rename %s to %s: %s'), 652 $tmpprojf, $projfile, $!]); 653 } 654 655} 656 657exit(0); 658 659 660 661 662