1#!/usr/bin/perl -w -U 2 3# Copyright (c) 2007, 2008 Andreas Gruenbacher. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 1. Redistributions of source code must retain the above copyright 10# notice, this list of conditions, and the following disclaimer, 11# without modification, immediately at the beginning of the file. 12# 2. The name of the author may not be used to endorse or promote products 13# derived from this software without specific prior written permission. 14# 15# Alternatively, this software may be distributed under the terms of the 16# GNU Public License ("GPL"). 17# 18# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR 22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 24# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 26# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 27# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 28# SUCH DAMAGE. 29# 30# $FreeBSD$ 31# 32 33# 34# Possible improvements: 35# 36# - distinguish stdout and stderr output 37# - add environment variable like assignments 38# - run up to a specific line 39# - resume at a specific line 40# 41 42use strict; 43use FileHandle; 44use Getopt::Std; 45use POSIX qw(isatty setuid getcwd); 46use vars qw($opt_l $opt_v); 47 48no warnings qw(taint); 49 50$opt_l = ~0; # a really huge number 51getopts('l:v'); 52 53my ($OK, $FAILED) = ("ok", "failed"); 54if (isatty(fileno(STDOUT))) { 55 $OK = "\033[32m" . $OK . "\033[m"; 56 $FAILED = "\033[31m\033[1m" . $FAILED . "\033[m"; 57} 58 59sub exec_test($$); 60sub process_test($$$$); 61 62my ($prog, $in, $out) = ([], [], []); 63my $prog_line = 0; 64my ($tests, $failed) = (0,0); 65my $lineno; 66my $width = ($ENV{COLUMNS} || 80) >> 1; 67 68for (;;) { 69 my $line = <>; $lineno++; 70 if (defined $line) { 71 # Substitute %VAR and %{VAR} with environment variables. 72 $line =~ s[%(\w+)][$ENV{$1}]eg; 73 $line =~ s[%\{(\w+)\}][$ENV{$1}]eg; 74 } 75 if (defined $line) { 76 if ($line =~ s/^\s*< ?//) { 77 push @$in, $line; 78 } elsif ($line =~ s/^\s*> ?//) { 79 push @$out, $line; 80 } else { 81 process_test($prog, $prog_line, $in, $out); 82 last if $prog_line >= $opt_l; 83 84 $prog = []; 85 $prog_line = 0; 86 } 87 if ($line =~ s/^\s*\$ ?//) { 88 $prog = [ map { s/\\(.)/$1/g; $_ } split /(?<!\\)\s+/, $line ]; 89 $prog_line = $lineno; 90 $in = []; 91 $out = []; 92 } 93 } else { 94 process_test($prog, $prog_line, $in, $out); 95 last; 96 } 97} 98 99my $status = sprintf("%d commands (%d passed, %d failed)", 100 $tests, $tests-$failed, $failed); 101if (isatty(fileno(STDOUT))) { 102 if ($failed) { 103 $status = "\033[31m\033[1m" . $status . "\033[m"; 104 } else { 105 $status = "\033[32m" . $status . "\033[m"; 106 } 107} 108print $status, "\n"; 109exit $failed ? 1 : 0; 110 111 112sub process_test($$$$) { 113 my ($prog, $prog_line, $in, $out) = @_; 114 115 return unless @$prog; 116 117 my $p = [ @$prog ]; 118 print "[$prog_line] \$ ", join(' ', 119 map { s/\s/\\$&/g; $_ } @$p), " -- "; 120 my $result = exec_test($prog, $in); 121 my @good = (); 122 my $nmax = (@$out > @$result) ? @$out : @$result; 123 for (my $n=0; $n < $nmax; $n++) { 124 my $use_re; 125 if (defined $out->[$n] && $out->[$n] =~ /^~ /) { 126 $use_re = 1; 127 $out->[$n] =~ s/^~ //g; 128 } 129 130 if (!defined($out->[$n]) || !defined($result->[$n]) || 131 (!$use_re && $result->[$n] ne $out->[$n]) || 132 ( $use_re && $result->[$n] !~ /^$out->[$n]/)) { 133 push @good, ($use_re ? '!~' : '!='); 134 } 135 else { 136 push @good, ($use_re ? '=~' : '=='); 137 } 138 } 139 my $good = !(grep /!/, @good); 140 $tests++; 141 $failed++ unless $good; 142 print $good ? $OK : $FAILED, "\n"; 143 if (!$good || $opt_v) { 144 for (my $n=0; $n < $nmax; $n++) { 145 my $l = defined($out->[$n]) ? $out->[$n] : "~"; 146 chomp $l; 147 my $r = defined($result->[$n]) ? $result->[$n] : "~"; 148 chomp $r; 149 print sprintf("%-" . ($width-3) . "s %s %s\n", 150 $r, $good[$n], $l); 151 } 152 } 153} 154 155 156sub su($) { 157 my ($user) = @_; 158 159 $user ||= "root"; 160 161 my ($login, $pass, $uid, $gid) = getpwnam($user) 162 or return [ "su: user $user does not exist\n" ]; 163 my @groups = (); 164 my $fh = new FileHandle("/etc/group") 165 or return [ "opening /etc/group: $!\n" ]; 166 while (<$fh>) { 167 chomp; 168 my ($group, $passwd, $gid, $users) = split /:/; 169 foreach my $u (split /,/, $users) { 170 push @groups, $gid 171 if ($user eq $u); 172 } 173 } 174 $fh->close; 175 176 my $groups = join(" ", ($gid, $gid, @groups)); 177 #print STDERR "[[$groups]]\n"; 178 $! = 0; # reset errno 179 $> = 0; 180 $( = $gid; 181 $) = $groups; 182 if ($!) { 183 return [ "su: $!\n" ]; 184 } 185 if ($uid != 0) { 186 $> = $uid; 187 #$< = $uid; 188 if ($!) { 189 return [ "su: $prog->[1]: $!\n" ]; 190 } 191 } 192 #print STDERR "[($>,$<)($(,$))]"; 193 return []; 194} 195 196 197sub sg($) { 198 my ($group) = @_; 199 200 my $gid = getgrnam($group) 201 or return [ "sg: group $group does not exist\n" ]; 202 my %groups = map { $_ eq $gid ? () : ($_ => 1) } (split /\s/, $)); 203 204 #print STDERR "<<", join("/", keys %groups), ">>\n"; 205 my $groups = join(" ", ($gid, $gid, keys %groups)); 206 #print STDERR "[[$groups]]\n"; 207 $! = 0; # reset errno 208 if ($> != 0) { 209 my $uid = $>; 210 $> = 0; 211 $( = $gid; 212 $) = $groups; 213 $> = $uid; 214 } else { 215 $( = $gid; 216 $) = $groups; 217 } 218 if ($!) { 219 return [ "sg: $!\n" ]; 220 } 221 print STDERR "[($>,$<)($(,$))]"; 222 return []; 223} 224 225 226sub exec_test($$) { 227 my ($prog, $in) = @_; 228 local (*IN, *IN_DUP, *IN2, *OUT_DUP, *OUT, *OUT2); 229 my $needs_shell = (join('', @$prog) =~ /[][|<>"'`\$\*\?]/); 230 231 if ($prog->[0] eq "umask") { 232 umask oct $prog->[1]; 233 return []; 234 } elsif ($prog->[0] eq "cd") { 235 if (!chdir $prog->[1]) { 236 return [ "chdir: $prog->[1]: $!\n" ]; 237 } 238 $ENV{PWD} = getcwd; 239 return []; 240 } elsif ($prog->[0] eq "su") { 241 return su($prog->[1]); 242 } elsif ($prog->[0] eq "sg") { 243 return sg($prog->[1]); 244 } elsif ($prog->[0] eq "export") { 245 my ($name, $value) = split /=/, $prog->[1]; 246 # FIXME: need to evaluate $value, so that things like this will work: 247 # export dir=$PWD/dir 248 $ENV{$name} = $value; 249 return []; 250 } elsif ($prog->[0] eq "unset") { 251 delete $ENV{$prog->[1]}; 252 return []; 253 } 254 255 pipe *IN2, *OUT 256 or die "Can't create pipe for reading: $!"; 257 open *IN_DUP, "<&STDIN" 258 or *IN_DUP = undef; 259 open *STDIN, "<&IN2" 260 or die "Can't duplicate pipe for reading: $!"; 261 close *IN2; 262 263 open *OUT_DUP, ">&STDOUT" 264 or die "Can't duplicate STDOUT: $!"; 265 pipe *IN, *OUT2 266 or die "Can't create pipe for writing: $!"; 267 open *STDOUT, ">&OUT2" 268 or die "Can't duplicate pipe for writing: $!"; 269 close *OUT2; 270 271 *STDOUT->autoflush(); 272 *OUT->autoflush(); 273 274 $SIG{CHLD} = 'IGNORE'; 275 276 if (fork()) { 277 # Server 278 if (*IN_DUP) { 279 open *STDIN, "<&IN_DUP" 280 or die "Can't duplicate STDIN: $!"; 281 close *IN_DUP 282 or die "Can't close STDIN duplicate: $!"; 283 } 284 open *STDOUT, ">&OUT_DUP" 285 or die "Can't duplicate STDOUT: $!"; 286 close *OUT_DUP 287 or die "Can't close STDOUT duplicate: $!"; 288 289 foreach my $line (@$in) { 290 #print "> $line"; 291 print OUT $line; 292 } 293 close *OUT 294 or die "Can't close pipe for writing: $!"; 295 296 my $result = []; 297 while (<IN>) { 298 #print "< $_"; 299 if ($needs_shell) { 300 s#^/bin/sh: line \d+: ##; 301 } 302 push @$result, $_; 303 } 304 return $result; 305 } else { 306 # Client 307 $< = $>; 308 close IN 309 or die "Can't close read end for input pipe: $!"; 310 close OUT 311 or die "Can't close write end for output pipe: $!"; 312 close OUT_DUP 313 or die "Can't close STDOUT duplicate: $!"; 314 local *ERR_DUP; 315 open ERR_DUP, ">&STDERR" 316 or die "Can't duplicate STDERR: $!"; 317 open STDERR, ">&STDOUT" 318 or die "Can't join STDOUT and STDERR: $!"; 319 320 if ($needs_shell) { 321 exec ('/bin/sh', '-c', join(" ", @$prog)); 322 } else { 323 exec @$prog; 324 } 325 print STDERR $prog->[0], ": $!\n"; 326 exit; 327 } 328} 329 330