mgetgroups: reduce duplicate listings
authorEric Blake <ebb9@byu.net>
Fri, 4 Dec 2009 21:07:58 +0000 (14:07 -0700)
committerEric Blake <ebb9@byu.net>
Mon, 7 Dec 2009 13:24:44 +0000 (06:24 -0700)
POSIX doesn't guarantee whether the effective gid is included in
the list of supplementary groups returned by getgroups.  On the
other hand, some platforms include the effective gid twice in
the list.  Meanwhile, mgetgroups can independently add a duplicate.
Rather than spend a full-blown O(n log n) cleanup, we just remove
the most common forms of duplicate groups with an O(n) pass.

* lib/mgetgroups.c (mgetgroups): Reduce duplicates from the
resulting array.
* tests/test-chown.h (test_chown): Simplify client.
* tests/test-lchown.h (test_lchown): Likewise.

Signed-off-by: Eric Blake <ebb9@byu.net>
ChangeLog
lib/mgetgroups.c
tests/test-chown.h
tests/test-lchown.h

index 977285c1fb03ec66ae90f2ca9a67039a4385e111..00ac8868c7b607242c49ef0f783438414afc8ed0 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,11 @@
+2009-12-07  Eric Blake  <ebb9@byu.net>
+
+       mgetgroups: reduce duplicate listings
+       * lib/mgetgroups.c (mgetgroups): Reduce duplicates from the
+       resulting array.
+       * tests/test-chown.h (test_chown): Simplify client.
+       * tests/test-lchown.h (test_lchown): Likewise.
+
 2009-12-06  Bruno Haible  <bruno@clisp.org>
 
        * lib/cloexec.c (dup_cloexec): Fix handling of _gl_register_dup return
index 7a61db41d900661e3542ea650429a6b3c94405ab..5ca450f9da9fd3bc5c99e2a9ee7134cc2bdad483 100644 (file)
@@ -53,7 +53,8 @@ realloc_groupbuf (gid_t *g, size_t num)
    NULL, store the supplementary groups of the current process, and GID
    should be -1 or the effective group ID (getegid).  Upon failure,
    don't modify *GROUPS, set errno, and return -1.  Otherwise, return
-   the number of groups.  */
+   the number of groups.  The resulting list may contain duplicates,
+   but adjacent members will be distinct.  */
 
 int
 mgetgroups (char const *username, gid_t gid, gid_t **groups)
@@ -157,6 +158,37 @@ mgetgroups (char const *username, gid_t gid, gid_t **groups)
       ng++;
     }
   *groups = g;
+
+  /* Reduce the number of duplicates.  On some systems, getgroups
+     returns the effective gid twice: once as the first element, and
+     once in its position within the supplementary groups.  On other
+     systems, getgroups does not return the effective gid at all,
+     which is why we provide a GID argument.  Meanwhile, the GID
+     argument, if provided, is typically any member of the
+     supplementary groups, and not necessarily the effective gid.  So,
+     the most likely duplicates are the first element with an
+     arbitrary other element, or pair-wise duplication between the
+     first and second elements returned by getgroups.  It is possible
+     that this O(n) pass will not remove all duplicates, but it is not
+     worth the effort to slow down to an O(n log n) algorithm that
+     sorts the array in place, nor the extra memory needed for
+     duplicate removal via an O(n) hash-table.  Hence, this function
+     is only documented as guaranteeing no pair-wise duplicates,
+     rather than returning the minimal set.  */
+  {
+    gid_t first = *g;
+    gid_t *next;
+    gid_t *sentinel = g + ng;
+
+    for (next = g + 1; next < sentinel; next++)
+      {
+        if (*next == first || *next == *g)
+          ng--;
+        else
+          *++g = *next;
+      }
+  }
+
   return ng;
 }
 
index 098b77d22bf30434952ef96e6960f355a8e804d2..c4e652af8961b4a401e26a7da40b3cc57379e821 100644 (file)
@@ -170,21 +170,12 @@ test_chown (int (*func) (char const *, uid_t, gid_t), bool print)
      changing group ownership of a file we own.  If we belong to at
      least two groups, then verifying the correct change is simple.
      But if we belong to only one group, then we fall back on the
-     other observable effect of chown: the ctime must be updated.
-     Be careful of duplicates returned by getgroups.  */
-  gids_count = mgetgroups (NULL, -1, &gids);
-  if (2 <= gids_count && gids[0] == gids[1] && 2 < gids_count--)
-    gids[1] = gids[2];
-  if (1 < gids_count || (gids_count == 1 && gids[0] != st1.st_gid))
+     other observable effect of chown: the ctime must be updated.  */
+  gids_count = mgetgroups (NULL, st1.st_gid, &gids);
+  if (1 < gids_count)
     {
-      if (gids[0] == st1.st_gid)
-        {
-          ASSERT (1 < gids_count);
-          ASSERT (gids[0] != gids[1]);
-          gids[0] = gids[1];
-        }
-      ASSERT (gids[0] != st1.st_gid);
-      ASSERT (gids[0] != (gid_t) -1);
+      ASSERT (gids[1] != st1.st_gid);
+      ASSERT (gids[1] != (gid_t) -1);
       ASSERT (lstat (BASE "dir/link", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
@@ -193,7 +184,7 @@ test_chown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_gid == st2.st_gid);
 
       errno = 0;
-      ASSERT (func (BASE "dir/link2/", -1, gids[0]) == -1);
+      ASSERT (func (BASE "dir/link2/", -1, gids[1]) == -1);
       ASSERT (errno == ENOTDIR);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
@@ -205,10 +196,10 @@ test_chown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
 
-      ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link2", -1, gids[1]) == 0);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
-      ASSERT (gids[0] == st2.st_gid);
+      ASSERT (gids[1] == st2.st_gid);
       ASSERT (lstat (BASE "dir/link", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
@@ -238,7 +229,7 @@ test_chown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (l2.st_ctime == st2.st_ctime);
       ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2));
 
-      ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link2", -1, st1.st_gid) == 0);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_ctime < st2.st_ctime
               || (st1.st_ctime == st2.st_ctime
index 43690ac4c68915160c7964f080f773284946d92b..93a657cf5d5df3775e22b1408d2a615e555c4680 100644 (file)
@@ -189,21 +189,12 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
      changing group ownership of a file we own.  If we belong to at
      least two groups, then verifying the correct change is simple.
      But if we belong to only one group, then we fall back on the
-     other observable effect of lchown: the ctime must be updated.
-     Be careful of duplicates returned by getgroups.  */
-  gids_count = mgetgroups (NULL, -1, &gids);
-  if (2 <= gids_count && gids[0] == gids[1] && 2 < gids_count--)
-    gids[1] = gids[2];
-  if (1 < gids_count || (gids_count == 1 && gids[0] != st1.st_gid))
+     other observable effect of lchown: the ctime must be updated.  */
+  gids_count = mgetgroups (NULL, st1.st_gid, &gids);
+  if (1 < gids_count)
     {
-      if (gids[0] == st1.st_gid)
-        {
-          ASSERT (1 < gids_count);
-          ASSERT (gids[0] != gids[1]);
-          gids[0] = gids[1];
-        }
-      ASSERT (gids[0] != st1.st_gid);
-      ASSERT (gids[0] != (gid_t) -1);
+      ASSERT (gids[1] != st1.st_gid);
+      ASSERT (gids[1] != (gid_t) -1);
       ASSERT (lstat (BASE "dir/link", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
@@ -212,7 +203,7 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_gid == st2.st_gid);
 
       errno = 0;
-      ASSERT (func (BASE "dir/link2/", -1, gids[0]) == -1);
+      ASSERT (func (BASE "dir/link2/", -1, gids[1]) == -1);
       ASSERT (errno == ENOTDIR);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
@@ -224,7 +215,7 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
 
-      ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link2", -1, gids[1]) == 0);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
@@ -233,7 +224,7 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_gid == st2.st_gid);
       ASSERT (lstat (BASE "dir/link2", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
-      ASSERT (gids[0] == st2.st_gid);
+      ASSERT (gids[1] == st2.st_gid);
 
       /* Trailing slash follows through to directory.  */
       ASSERT (lstat (BASE "dir/link3", &st2) == 0);
@@ -243,13 +234,13 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
 
-      ASSERT (func (BASE "dir/link3/", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link3/", -1, gids[1]) == 0);
       ASSERT (lstat (BASE "dir/link3", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
       ASSERT (st1.st_gid == st2.st_gid);
       ASSERT (lstat (BASE "dir/sub", &st2) == 0);
       ASSERT (st1.st_uid == st2.st_uid);
-      ASSERT (gids[0] == st2.st_gid);
+      ASSERT (gids[1] == st2.st_gid);
     }
   else if (!CHOWN_CHANGE_TIME_BUG || HAVE_LCHMOD)
     {
@@ -275,7 +266,7 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (l2.st_ctime == st2.st_ctime);
       ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2));
 
-      ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link2", -1, st1.st_gid) == 0);
       ASSERT (stat (BASE "dir/file", &st2) == 0);
       ASSERT (st1.st_ctime == st2.st_ctime);
       ASSERT (get_stat_ctime_ns (&st1) == get_stat_ctime_ns (&st2));
@@ -291,7 +282,7 @@ test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
       ASSERT (lstat (BASE "dir/sub", &st1) == 0);
       ASSERT (lstat (BASE "dir/link3", &l1) == 0);
       nap ();
-      ASSERT (func (BASE "dir/link3/", -1, gids[0]) == 0);
+      ASSERT (func (BASE "dir/link3/", -1, st1.st_gid) == 0);
       ASSERT (lstat (BASE "dir/link3", &st2) == 0);
       ASSERT (l1.st_ctime == st2.st_ctime);
       ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));