1#!./perl
2BEGIN {
3    if ( $^O eq 'VMS' ) {
4        my $p = "/bin:/usr/bin:/usr/xpg4/bin:/usr/ucb";
5        if ( $ENV{PATH} ) {
6            $p .= ":$ENV{PATH}";
7        }
8        $ENV{PATH} = $p;
9    }
10    $ENV{LC_ALL} = "C"; # so that external utilities speak English
11    $ENV{LANGUAGE} = 'C'; # GNU locale extension
12
13    chdir 't' if -d 't';
14    require './test.pl';
15    set_up_inc( '../lib' );
16    skip_all_if_miniperl("no dynamic loading on miniperl, no POSIX");
17}
18
19use 5.010;
20use strict;
21use Config ();
22use POSIX ();
23
24skip_all('getgrgid() not implemented')
25    unless eval { my($foo) = getgrgid(0); 1 };
26
27skip_all("No 'id' or 'groups'") if
28    $^O eq 'MSWin32' || $^O eq 'VMS' || $^O =~ /lynxos/i;
29
30Test();
31exit;
32
33
34
35sub Test {
36
37    # Get our supplementary groups from the system by running commands
38    # like `id -a'.
39    my ( $groups_command, $groups_string ) = system_groups()
40        or skip_all("No 'id' or 'groups'");
41    my @extracted_groups = extract_system_groups( $groups_string )
42        or skip_all("Can't parse '${groups_command}'");
43
44    my $pwgid = $( + 0;
45    my ($pwgnam) = getgrgid($pwgid);
46    $pwgnam //= '';
47    note "pwgid=$pwgid pwgnam=$pwgnam \$(=$(";
48
49    # Get perl's supplementary groups by looking at $(
50    my ( $gid_count, $all_perl_groups ) = perl_groups();
51    my %basegroup = basegroups( $pwgid, $pwgnam );
52    my @extracted_supplementary_groups = remove_basegroup( \ %basegroup, \ @extracted_groups );
53
54    plan 4;
55
56    {
57        my @warnings = do {
58            my @w;
59            local $SIG{'__WARN__'} = sub { push @w, @_ };
60
61            use warnings;
62            my $v = $( + 1;
63            $v = $) + 1;
64
65            @w;
66        };
67
68        is ("@warnings", "", 'Neither $( nor $) trigger warnings when used as number.' );
69    }
70
71    # Test: The supplementary groups in $( should match the
72    # getgroups(2) kernal API call.
73    #
74    SKIP: {
75        my $ngroups_max = posix_ngroups_max();
76        if ( defined $ngroups_max && $ngroups_max < @extracted_groups ) {
77            # Some OSes (like darwin)but conceivably others might return
78            # more groups from `id -a' than can be handled by the
79            # kernel. On darwin, NGROUPS_MAX is 16 and 12 are taken up for
80            # the system already.
81            #
82            # There is more fall-out from this than just Perl's unit
83            # tests. You may be a member of a group according to Active
84            # Directory (or whatever) but the OS won't respect it because
85            # it's the 17th (or higher) group and there's no space to
86            # store your membership.
87            skip "Your platform's `$groups_command' is broken";
88        }
89
90        if ( darwin() ) {
91            # darwin uses getgrouplist(3) or an Open Directory API within
92            # /usr/bin/id and /usr/bin/groups which while "nice" isn't
93            # accurate for this test. The hard, real, list of groups we're
94            # running in derives from getgroups(2) and is not dynamic but
95            # the Libc API getgrouplist(3) is.
96            #
97            # In practical terms, this meant that while `id -a' can be
98            # relied on in other OSes to purely use getgroups(2) and show
99            # us what's real, darwin will use getgrouplist(3) to show us
100            # what might be real if only we'd open a new console.
101            #
102            skip "darwin's `${groups_command}' can't be trusted";
103        }
104
105        # Read $( but ignore any groups in $( that we failed to parse
106        # successfully out of the `id -a` mess.
107        #
108        my @perl_groups = remove_unparsed_entries( \ @extracted_groups,
109                                                   \ @$all_perl_groups );
110        my @supplementary_groups = remove_basegroup( \ %basegroup,
111                                                     \ @perl_groups );
112
113        my $ok1 = 0;
114        if ( match_groups( \ @supplementary_groups,
115                           \ @extracted_supplementary_groups,
116                           $pwgid ) ) {
117            $ok1 = 1;
118        }
119        elsif ( cygwin_nt() ) {
120            %basegroup = unixy_cygwin_basegroups();
121            @extracted_supplementary_groups = remove_basegroup( \ %basegroup, \ @extracted_groups );
122
123            if ( match_groups( \ @supplementary_groups,
124                               \ @extracted_supplementary_groups,
125                               $pwgid ) ) {
126                note "This Cygwin behaves like Unix (Win2k?)";
127                $ok1 = 1;
128            }
129        }
130
131        ok $ok1, "perl's `\$(' agrees with `${groups_command}'";
132    }
133
134    # multiple 0's indicate GROUPSTYPE is currently long but should be short
135    $gid_count->{0} //= 0;
136    ok 0 == $pwgid || $gid_count->{0} < 2, "groupstype should be type short, not long";
137
138    SKIP: {
139        # try to add a group as supplementary group
140        my $root_uid = 0;
141        skip "uid!=0", 1 if $< != $root_uid and $> != $root_uid;
142        my @groups = split ' ', $);
143        my @sup_group;
144        setgrent;
145        while(my @ent = getgrent) {
146            next if grep { $_ == $ent[2] } @groups;
147            @sup_group = @ent;
148            last;
149        }
150        endgrent;
151        skip "No group found we could add as a supplementary group", 1
152            if (!@sup_group);
153        $) = "$) $sup_group[2]";
154        my $ok = grep { $_ == $sup_group[2] } split ' ', $);
155        ok $ok, "Group `$sup_group[0]' added as supplementary group";
156    }
157
158    return;
159}
160
161# Get the system groups and the command used to fetch them.
162#
163sub system_groups {
164    my ( $cmd, $groups_string ) = _system_groups();
165
166    if ( $groups_string ) {
167        chomp $groups_string;
168        diag_variable( groups => $groups_string );
169    }
170
171    return ( $cmd, $groups_string );
172}
173
174# We have to find a command that prints all (effective
175# and real) group names (not ids).  The known commands are:
176# groups
177# id -Gn
178# id -a
179# Beware 1: some systems do just 'id -G' even when 'id -Gn' is used.
180# Beware 2: id -Gn or id -a format might be id(name) or name(id).
181# Beware 3: the groups= might be anywhere in the id output.
182# Beware 4: groups can have spaces ('id -a' being the only defense against this)
183# Beware 5: id -a might not contain the groups= part.
184#
185# That is, we might meet the following:
186#
187# foo bar zot				# accept
188# foo 22 42 bar zot			# accept
189# 1 22 42 2 3				# reject
190# groups=(42),foo(1),bar(2),zot me(3)	# parsed by $GROUP_RX1
191# groups=22,42,1(foo),2(bar),3(zot(me))	# parsed by $GROUP_RX2
192#
193# and the groups= might be after, before, or between uid=... and gid=...
194use constant GROUP_RX1 => qr/
195    ^
196    (?<gr_name>.+)
197    \(
198        (?<gid>\d+)
199    \)
200    $
201/x;
202use constant GROUP_RX2 => qr/
203    ^
204    (?<gid>\d+)
205    \(
206        (?<gr_name>.+)
207    \)
208    $
209/x;
210sub _system_groups {
211    my $cmd;
212    my $str;
213
214    # prefer 'id' over 'groups' (is this ever wrong anywhere?)
215    # and 'id -a' over 'id -Gn' (the former is good about spaces in group names)
216
217    $cmd = 'id -a 2>/dev/null || id 2>/dev/null';
218    $str = `$cmd`;
219    if ( $str && $str =~ /groups=/ ) {
220        # $str is of the form:
221        # uid=39957(gsar) gid=22(users) groups=33536,39181,22(users),0(root),1067(dev)
222        # FreeBSD since 6.2 has a fake id -a:
223        # uid=1001(tobez) gid=20(staff) groups=20(staff), 0(wheel), 68(dialer)
224        # On AIX it's id
225        #
226        # Linux may also have a context= field
227
228        return ( $cmd, $str );
229    }
230
231    $cmd = 'id -Gn 2>/dev/null';
232    $str = `$cmd`;
233    if ( $str && $str !~ /^[\d\s]$/ ) {
234        # $str could be of the form:
235        # users 33536 39181 root dev
236        return ( $cmd, $str );
237    }
238
239    $cmd = 'groups 2>/dev/null';
240    $str = `$cmd`;
241    if ( $str ) {
242        # may not reflect all groups in some places, so do a sanity check
243        if (-d '/afs') {
244            print <<EOM;
245# These test results *may* be bogus, as you appear to have AFS,
246# and I can't find a working 'id' in your PATH (which I have set
247# to '$ENV{PATH}').
248#
249# If these tests fail, report the particular incantation you use
250# on this platform to find *all* the groups that an arbitrary
251# user may belong to, using the issue tracker.
252EOM
253        }
254        return ( $cmd, $str );
255    }
256
257    return ();
258}
259
260# Convert the strings produced by parsing `id -a' into a list of group
261# names
262sub extract_system_groups {
263    my ( $groups_string ) = @_;
264
265    # Remember that group names can contain whitespace, '-', '(parens)',
266    # et cetera. That is: do not \w, do not \S.
267    my @extracted;
268
269    my @fields = split /\b(\w+=)/, $groups_string;
270    my $gr;
271    for my $i (0..@fields-2) {
272        if ($fields[$i] eq 'groups=') {
273            $gr = $fields[$i+1];
274            $gr =~ s/ $//;
275            last;
276        }
277    }
278    if (defined $gr) {
279        my @g = split m{, ?}, $gr;
280        # prefer names over numbers
281        for (@g) {
282            if ( $_ =~ GROUP_RX1() || $_ =~ GROUP_RX2() ) {
283                push @extracted, $+{gr_name} || $+{gid};
284            }
285            else {
286                note "ignoring group entry [$_]";
287            }
288        }
289
290        diag_variable( gr => $gr );
291        diag_variable( g => join ',', @g );
292        diag_variable( ex_gr => join ',', @extracted );
293    }
294
295    return @extracted;
296}
297
298# Get the POSIX value NGROUPS_MAX.
299sub posix_ngroups_max {
300    return eval {
301        POSIX::NGROUPS_MAX();
302    };
303}
304
305# Test if this is Apple's darwin
306sub darwin {
307    # Observed 'darwin-2level'
308    return $Config::Config{myuname} =~ /^darwin/;
309}
310
311# Test if this is Cygwin
312sub cygwin_nt {
313    return $Config::Config{myuname} =~ /^cygwin_nt/i;
314}
315
316# Get perl's supplementary groups and the number of times each gid
317# appeared.
318sub perl_groups {
319    # Lookup perl's own groups from $(
320    my @gids = split ' ', $(;
321    my %gid_count;
322    my @gr_name;
323    for my $gid ( @gids ) {
324        ++ $gid_count{$gid};
325
326        my ($group) = getgrgid $gid;
327
328        # Why does this test prefer to not test groups which we don't have
329        # a name for? One possible answer is that my primary group comes
330        # from my entry in the user database but isn't mentioned in
331        # the group database.  Are there more reasons?
332        next if ! defined $group;
333
334
335        push @gr_name, $group;
336    }
337
338    diag_variable( gr_name => join ',', @gr_name );
339
340    return ( \ %gid_count, \ @gr_name );
341}
342
343# Remove entries from our parsing of $( that don't appear in our
344# parsing of `id -a`.
345sub remove_unparsed_entries {
346    my ( $extracted_groups, $perl_groups ) = @_;
347
348    my %was_extracted =
349        map { $_ => 1 }
350        @$extracted_groups;
351
352    return
353        grep { $was_extracted{$_} }
354        @$perl_groups;
355}
356
357# Get a list of base groups. I'm not sure why cygwin by default is
358# skipped here.
359sub basegroups {
360    my ( $pwgid, $pwgnam ) = @_;
361
362    if ( cygwin_nt() ) {
363        return;
364    }
365    else {
366        return (
367            $pwgid  => 1,
368            $pwgnam => 1,
369        );
370    }
371}
372
373# Cygwin might have another form of basegroup which we should actually use
374sub unixy_cygwin_basegroups {
375    my ( $pwgid, $pwgnam ) = @_;
376    return (
377        $pwgid  => 1,
378        $pwgnam => 1,
379    );
380}
381
382# Filter a full list of groups and return only the supplementary
383# gorups.
384sub remove_basegroup {
385    my ( $basegroups, $groups ) = @_;
386
387    return
388        grep { ! $basegroups->{$_} }
389        @$groups;
390}
391
392# Test supplementary groups to see if they're a close enough match or
393# if there aren't any supplementary groups then validate the current
394# group against $(.
395sub match_groups {
396    my ( $supplementary_groups, $extracted_supplementary_groups, $pwgid ) = @_;
397
398    # Compare perl vs system groups
399    my %g;
400    $g{$_}[0] = 1 for @$supplementary_groups;
401    $g{$_}[1] = 1 for @$extracted_supplementary_groups;
402
403    # Find any mismatches
404    my @misses =
405        grep { ! ( $g{$_}[0] && $g{$_}[1] ) }
406        sort keys %g;
407
408    return
409        ! @misses
410        || ( ! @$supplementary_groups
411             && 1 == @$extracted_supplementary_groups
412             && $pwgid == $extracted_supplementary_groups->[0] );
413}
414
415# Print a nice little diagnostic.
416sub diag_variable {
417    my ( $label, $content ) = @_;
418
419    printf "# %-11s=%s\n", $label, $content;
420    return;
421}
422
423# Removes duplicates from a list
424sub uniq {
425    my %seen;
426    return
427        grep { ! $seen{$_}++ }
428        @_;
429}
430
431# ex: set ts=8 sts=4 sw=4 et:
432