#! /bin/sh # cvs-fix-rep-perm --- fix cvs repository permissions # Author: Noah Friedman # Created: 1997-08-20 # $Id: cvs-fix-rep-perm,v 1.3 1998/06/01 01:56:15 friedman Exp $ # Commentary: # If you want to use this script in the backend to CVS so that file # permissions are automatically corrected when changes are committed, # Add a line like the following to $CVSROOT/CVSROOT/loginfo: # # ALL exec $CVSROOT/CVSROOT/cvs-fix-rep-perm -L %s # # assuming you install this script with your other administrative files. # Make sure perl is in your path or use the -P option. # You may wish to add other options as appropriate. # Code: # Name by which this script was invoked. progname=`echo "$0" | sed -e 's/[^\/]*\///g'` # To prevent hairy quoting and escaping later. bq='`' eq="'" usage="Usage: $progname {options} Options are: -D, --debug Turn on shell debugging ($bq${bq}set -x$eq$eq). -g, --group GROUP Change group of all files to GROUP. -h, --help You're looking at it. -L, --cvs-loginfo This option is only used when invoked via the CVS ${bq}loginfo$eq administrative back end. This option may imply $bq--recursive$eq for some kinds of CVS operations (e.g. import). -m, --directory-mode PERM Change mode of directories to PERM. This should be a numeric argument in octal. -P, --perl PERL (path)name of perl interpreter to use. -r, --recursive Descend recursively into directories. -u, --umask UMASK Mask out these permission bits from files in the respository. This should be a numeric argument in octal. Note that this affects files only, not directories. Use the $bq-p$eq option to set directory permissions. -v, --verbose Print file names as they are modified. " # Initialize variables. # Don't use `unset' since old bourne shells don't have this command. # Instead, assign them an empty value. debug= perl="${PERL-perl}" CFRP_GROUP= CFRP_DIRPERM=02777 CFRP_LOGINFO= CFRP_PROGNAME="$progname" CFRP_RECURSIVE= CFRP_UMASK=07222 CFRP_VERBOSE= export CFRP_GROUP CFRP_DIRPERM CFRP_LOGINFO CFRP_PROGNAME export CFRP_RECURSIVE CFRP_UMASK CFRP_VERBOSE # Usage: eval "$getopt"; value=$optarg # or optarg_optional=t; eval "$getopt"; value=$optarg # # This function automatically shifts the positional args as appropriate. # The argument to an option is optional if the variable `optarg_optional' # is non-empty. Otherwise, the argument is required and getopt will cause # the program to exit on an error. optarg_optional is reset to be empty # after every call to getopt. The argument (if any) is stored in the # variable `optarg'. # # Long option syntax is `--foo=bar' or `--foo bar'. # For optional args, you must use the `--foo=bar' long option syntax # if the argument starts with `-', otherwise the argument will be ignored # and treated as the next option. # # Note: because of broken bourne shells, using --foo=bar syntax can # actually screw the quoting of args that end with trailing newlines. # Specifically, most shells strip trailing newlines from substituted # output, regardless of quoting. getopt=' { optarg= case "$1" in --*=* ) optarg=`echo "$1" | sed -e "1s/^[^=]*=//"` ; shift ;; -* ) case "${2+set}:$optarg_optional" in set: ) optarg="$2" ; shift ; shift ;; set:?* ) case "$2" in -* ) shift ;; * ) optarg="$2"; shift; shift ;; esac ;; : ) option="$1" case "$option" in --*=* ) option=`echo "$option" | sed -e "1s/=.*//;q"` ;; esac echo "$progname: option $bq$option$eq requires argument." 1>&2 echo "$progname: use $bq--help$eq to list option syntax." 1>&2 exit 1 ;; * ) shift ;; esac ;; esac optarg_optional= }' # Parse command line arguments. # Make sure that all wildcarded options are long enough to be unambiguous. # It's a good idea to document the full long option name in each case. # Long options which take arguments will need a `*' appended to the # canonical name to match the value appended after the `=' character. while : ; do case $# in 0) break ;; esac case "$1" in -D | --debug | --d* ) debug=-d shift ;; -L | --cvs-loginfo | --c* ) CFRP_LOGINFO=t shift ;; -h | --help | --h* ) echo "$usage" 1>&2 exit 0 ;; -g | --group* | --g* ) eval "$getopt" CFRP_GROUP="$optarg" ;; -m | --directory-mode* | --m* ) eval "$getopt" CFRP_DIRPERM="$optarg" ;; -P | --perl* | --p* ) eval "$getopt" perl="$optarg" ;; -r | --recursive | --r* ) CFRP_RECURSIVE=t shift ;; -u | --umask* | --u* | --file-umask* | --f* ) eval "$getopt" CFRP_UMASK="$optarg" ;; -v | --verbose | --v* ) CFRP_VERBOSE=t shift ;; -- ) # Stop option processing shift break ;; -? | --* ) case "$1" in --*=* ) arg=`echo "$1" | sed -e 's/=.*//'` ;; * ) arg="$1" ;; esac exec 1>&2 echo "$progname: unknown or ambiguous option $bq$arg$eq" echo "$progname: Use $bq--help$eq for a list of options." exit 1 ;; -??* ) # Split grouped single options into separate args and try again optarg="$1" shift set fnord `echo "x$optarg" | sed -e 's/^x-//;s/\(.\)/-\1 /g'` ${1+"$@"} shift ;; * ) break ;; esac done case "$debug" in -d ) set -x ;; esac # Sigh. If we happen to be running setuid, perl refuses to read scripts # from stdin, so use a tempfile instead. What a waste, and all in the same # of "security". CFRP_SCRIPTFILE="${TMPDIR-/tmp}/$progname$$" export CFRP_SCRIPTFILE trap 'exitstat=$? rm -f "$CFRP_SCRIPTFILE" trap "" 0 1 2 3 15 exit $exitstat ' 0 1 2 3 15 cat > "$CFRP_SCRIPTFILE" <<'__EOF__' &main (&untaint (@ARGV)); sub main { &untaint_env (); # Delete the temporary scriptfile, as a way of cleaning up after the # shell invocation. { local ($file) = &getenv ('CFRP_SCRIPTFILE'); unlink ($file) if (defined ($file)); } &disable_stdio_buffering (STDOUT, STDERR); $group = &getenv ("CFRP_GROUP"); $dir_mode = oct (&getenv ("CFRP_DIRPERM")); $file_umask = oct (&getenv ("CFRP_UMASK")); if (&getenv ("CFRP_LOGINFO")) { local ($cvsroot) = &getenv ("CVSROOT"); local ($module); &fatal ("CVSROOT env var not defined") if (! defined ($cvsroot)); ($module, @_) = parse_loginfo_args ($cvsroot, @_); &my_chdir ("$cvsroot/$module"); } if (&getenv ("CFRP_RECURSIVE")) { foreach $arg (@_) { &grind_over_tree ($arg, 'fix_perms'); } } else { &fix_perms (@_); } } # Parse args of the form: module file file file ... # where module is relative to the cvs root, and each file is a name # (probably sans `,v' suffix or Attic reference) relative to the module. # Return a list with the same relative module name followed by # corresponding file names that actually exist in the repository. # # Godforsaken cvs loginfo syntax specifies module name and all repository # files in a single argument, meaning that to properly parse file or # module names containing whitespace, we have a lot of work to do. # This still isn't correct; if someone tries to commit "foo bar" and # "foo" (in that order), then "foo bar" won't be parsed correctly; "foo" # will show up twice and "bar" will appear as a nonexistent file name. sub parse_loginfo_args { local ($cvsroot) = $_[0]; local ($module, @args); shift (@_); # strip off cvsroot if ($_[0] =~ / - New directory/o) { $_[0] =~ s/ - New directory.*//o; return @_; } if ($_[0] =~ / - Imported sources/o) { &putenv ('CFRP_RECURSIVE', 't'); $_[0] =~ s/ - Imported sources.*//o; push (@_, '.'); # update recursively all files in module return @_; } &pushdir ($cvsroot); while ($#_ >= 0) { local ($name, $rest) = split (/ /o, $_[0], 2); local ($new); while (! -e $name) { local ($found_alternate) = 0; foreach $try ("${name},v", "Attic/${name},v") { if (-e $try) { $name = $try; $found_alternate = 1; last; } } last if $found_alternate; ($new, $rest) = split (/ /o, $rest, 2); $name .= ' ' . $new if (defined ($new)); if (! -e $name && ! defined ($rest)) { # It looks like the file as named just doesn't exist; some # component somewhere is missing. In that case, just parse # the first word as a file name, then try again with the rest # of the arguments. ($name, $rest) = split (/ /o, $name, 2); last; } } shift (@_); unshift (@_, $rest) if (defined ($rest)); push (@args, $name) if ($name ne ''); # The first name is the module; in order to check relative file names # above we much chdir into the module directory. if (! defined ($module)) { $module = $name; &my_chdir ("$cvsroot/$module"); } } &popdir (); return @args; } sub grind_over_tree { local ($dir, $action) = @_; local (@files); &$action ($dir); return if (! -d $dir); if (! opendir (DIRHNDL, $dir)) { &err ("opendir", "$dir", "$!"); return; } @files = grep (!/^\.\.?$/, readdir (DIRHNDL)); close (DIRHNDL); foreach $ent (@files) { &grind_over_tree ("$dir/$ent", $action); } } sub fix_perms { foreach $ent (@_) { if (-d $ent) { &verbose ("D $ent"); do_chgrp ($group, $ent); do_chmod ($dir_mode, $ent); } else { &verbose ("F $ent"); do_chgrp ($group, $ent); &chmod_owner_perms_to_all ($file_umask, $ent); } } } sub chmod_owner_perms_to_all { local ($umask, @files) = @_; foreach $file (@files) { # $statinfo[2] == mode local (@statinfo) = stat ($file); local ($newmode); if (! defined (@statinfo)) { &err ("stat", $file, "$!"); next; } $newmode = &owner_perms_to_all ($umask, $statinfo[2]); if (chmod ($newmode, $file) == 0) { local ($snewmode) = sprintf ("0%o", $newmode); &err ("chmod($snewmode)", $file, "$!"); } } } sub owner_perms_to_all { local ($umask, $origmode) = @_; local ($newmode) = 0; local ($o_mask) = 0700; # constant local ($ugo_mask ) = 0777; # constant # mask out all but ugo bits, then copy `o' bits to `g' and `o'. $mode = $origmode & $o_mask; $newmode = $mode | ($mode >> 3) | ($mode >> 6); # Make sure ugo bits are zeroed out in old mode, then `or' in new bits. $mode = ($origmode | $ugo_mask) ^ $ugo_mask; $newmode = $mode | $newmode; # Strip out any bits prohibited by umask. return ($newmode | $umask) ^ $umask; } sub do_chmod { local ($perm, @files) = @_; local ($perm_str) = sprintf ("0%o", $perm); foreach $file (@files) { if (chmod ($perm, $file) == 0) { &err ("chmod($perm_str)", $file, "$!"); } } } sub do_chgrp { return if (! defined ($group)); local ($group, @files) = @_; local ($gid) = &grnam_to_gid ($group); if (! defined ($gid)) { &err ($group, "no such group"); return undef; } foreach $file (@files) { # Don't call chown on files that already have the gid we want, since # that can reset setuid/gid permission bits. next if ($gid == &gid_of_file ($file)); if (chown (-1, $gid, $file) == 0) { &err ("chgrp($group)", $file, "$!"); } } } sub gid_of_file { local (@stat) = stat ($_[0]); return $stat[5]; } sub grnam_to_gid { local ($group) = @_; $group = getgrnam ($group) if ($group !~ /^[0-9]+$/o); return $group; } sub my_chdir { &fatal ('chdir', $_[0], "$!") if (! chdir ($_[0])); } sub pushdir { local ($pwd) = `pwd`; &my_chdir (@_); chop $pwd; unshift (@dirstack, $pwd); } sub popdir { &fatal ('popdir', 'directory stack empty') if (! defined (@dirstack) || $#dirstack < 0); &my_chdir ($dirstack[0]); shift @dirstack; } sub getenv { return $ENV{$_[0]}; } sub putenv { $ENV{$_[0]} = $_[1]; } sub err { print (STDERR join(": ", &getenv ('CFRP_PROGNAME'), @_) . "\n"); } sub fatal { unshift (@_, '*** FATAL'); &err (@_); exit (1); } sub verbose { print (join(": ", @_) . "\n") if &getenv ("CFRP_VERBOSE"); } sub disable_stdio_buffering { foreach $handle (@_) { local ($orig_handle) = select ($handle); $| = 1; select ($orig_handle); } } # This "untaints" any values which may be suspect. # This only matters when running setuid. sub untaint { local (@y); foreach $z (@_) { $z =~ /^(.*)$/; push (@y, $1); # $1 is always untainted } return @y; } sub untaint_env { foreach $v (keys (%ENV)) { ($ENV{$v}) = &untaint ($ENV{$v}); } } __EOF__ case $? in 0 ) exec $perl $debug "$CFRP_SCRIPTFILE" ${1+"$@"} ;; esac # cvs-fix-rep-perm ends here