Minor fixups to port to Solaris 10 with Sun C 5.8.
[gnulib.git] / lib / getcwd.c
index 1bc7ab6..f8567bd 100644 (file)
@@ -1,5 +1,5 @@
-/* Copyright (C) 1991,92,93,94,95,96,97,98,99,2004 Free Software Foundation,
-   Inc.
+/* Copyright (C) 1991,92,93,94,95,96,97,98,99,2004,2005,2006,2007 Free Software
+   Foundation, Inc.
    This file is part of the GNU C Library.
 
    This program is free software; you can redistribute it and/or modify
 
    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation,
-   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
+   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
 
 #if !_LIBC
-# include "getcwd.h"
+# include <config.h>
+# include <unistd.h>
+# include "dirfd.h"
 #endif
 
 #include <errno.h>
 #include <stdbool.h>
 #include <stddef.h>
 
-#if HAVE_FCNTL_H
-# include <fcntl.h> /* For AT_FDCWD on Solaris 9.  */
-#endif
+#include <fcntl.h> /* For AT_FDCWD on Solaris 9.  */
 
 #ifndef __set_errno
 # define __set_errno(val) (errno = (val))
 #endif
 
-#if HAVE_DIRENT_H || _LIBC
-# include <dirent.h>
-# ifndef _D_EXACT_NAMLEN
-#  define _D_EXACT_NAMLEN(d) strlen ((d)->d_name)
-# endif
-#else
-# define dirent direct
-# if HAVE_SYS_NDIR_H
-#  include <sys/ndir.h>
-# endif
-# if HAVE_SYS_DIR_H
-#  include <sys/dir.h>
-# endif
-# if HAVE_NDIR_H
-#  include <ndir.h>
-# endif
-#endif
+#include <dirent.h>
 #ifndef _D_EXACT_NAMLEN
-# define _D_EXACT_NAMLEN(d) ((d)->d_namlen)
+# define _D_EXACT_NAMLEN(d) strlen ((d)->d_name)
 #endif
 #ifndef _D_ALLOC_NAMLEN
 # define _D_ALLOC_NAMLEN(d) (_D_EXACT_NAMLEN (d) + 1)
 #endif
 
-#if HAVE_UNISTD_H || _LIBC
-# include <unistd.h>
-#endif
-
+#include <unistd.h>
 #include <stdlib.h>
 #include <string.h>
 
 # ifndef mempcpy
 #  define mempcpy __mempcpy
 # endif
-#else
-# include "mempcpy.h"
 #endif
 
 #include <limits.h>
 
+/* Work around a bug in Solaris 9 and 10: AT_FDCWD is positive.  Its
+   value exceeds INT_MAX, so its use as an int doesn't conform to the
+   C standard, and GCC and Sun C complain in some cases.  */
+#if 0 < AT_FDCWD && AT_FDCWD == 0xffd19553
+# undef AT_FDCWD
+# define AT_FDCWD (-3041965)
+#endif
+
+#ifdef ENAMETOOLONG
+# define is_ENAMETOOLONG(x) ((x) == ENAMETOOLONG)
+#else
+# define is_ENAMETOOLONG(x) 0
+#endif
+
 #ifndef MAX
 # define MAX(a, b) ((a) < (b) ? (b) : (a))
 #endif
 # define __opendir opendir
 # define __readdir readdir
 #endif
+
+/* The results of opendir() in this file are not used with dirfd and fchdir,
+   therefore save some unnecessary recursion in fchdir.c.  */
+#undef opendir
+#undef closedir
 \f
-/* Get the pathname of the current working directory, and put it in SIZE
+/* Get the name of the current working directory, and put it in SIZE
    bytes of BUF.  Returns NULL if the directory couldn't be determined or
    SIZE was too small.  If successful, returns BUF.  In GNU, if BUF is
    NULL, an array is allocated with `malloc'; the array is SIZE bytes long,
@@ -141,11 +135,29 @@ __getcwd (char *buf, size_t size)
   DIR *dirstream = NULL;
   dev_t rootdev, thisdev;
   ino_t rootino, thisino;
-  char *path;
-  register char *pathp;
+  char *dir;
+  register char *dirp;
   struct stat st;
-  int prev_errno = errno;
   size_t allocated = size;
+  size_t used;
+
+#if HAVE_PARTLY_WORKING_GETCWD
+  /* The system getcwd works, except it sometimes fails when it
+     shouldn't, setting errno to ERANGE, ENAMETOOLONG, or ENOENT.  If
+     AT_FDCWD is not defined, the algorithm below is O(N**2) and this
+     is much slower than the system getcwd (at least on GNU/Linux).
+     So trust the system getcwd's results unless they look
+     suspicious.
+
+     Use the system getcwd even if we have openat support, since the
+     system getcwd works even when a parent is unreadable, while the
+     openat-based approach does not.  */
+
+# undef getcwd
+  dir = getcwd (buf, size);
+  if (dir || (errno != ERANGE && !is_ENAMETOOLONG (errno) && errno != ENOENT))
+    return dir;
+#endif
 
   if (size == 0)
     {
@@ -158,17 +170,17 @@ __getcwd (char *buf, size_t size)
       allocated = BIG_FILE_NAME_LENGTH + 1;
     }
 
-  if (buf != NULL)
-    path = buf;
-  else
+  if (buf == NULL)
     {
-      path = malloc (allocated);
-      if (path == NULL)
+      dir = malloc (allocated);
+      if (dir == NULL)
        return NULL;
     }
+  else
+    dir = buf;
 
-  pathp = path + allocated;
-  *--pathp = '\0';
+  dirp = dir + allocated;
+  *--dirp = '\0';
 
   if (__lstat (".", &st) < 0)
     goto lose;
@@ -187,6 +199,9 @@ __getcwd (char *buf, size_t size)
       ino_t dotino;
       bool mount_point;
       int parent_status;
+      size_t dirroom;
+      size_t namlen;
+      bool use_d_ino = true;
 
       /* Look at the parent directory.  */
 #ifdef AT_FDCWD
@@ -220,6 +235,8 @@ __getcwd (char *buf, size_t size)
       dirstream = fdopendir (fd);
       if (dirstream == NULL)
        goto lose;
+      /* Reset fd.  It may have been closed by fdopendir.  */
+      fd = dirfd (dirstream);
       fd_needs_closing = false;
 #else
       dirstream = __opendir (dotlist);
@@ -227,110 +244,131 @@ __getcwd (char *buf, size_t size)
        goto lose;
       dotlist[dotlen++] = '/';
 #endif
-      /* Clear errno to distinguish EOF from error if readdir returns
-        NULL.  */
-      __set_errno (0);
-      while ((d = __readdir (dirstream)) != NULL)
+      for (;;)
        {
+         /* Clear errno to distinguish EOF from error if readdir returns
+            NULL.  */
+         __set_errno (0);
+         d = __readdir (dirstream);
+
+         /* When we've iterated through all directory entries without finding
+            one with a matching d_ino, rewind the stream and consider each
+            name again, but this time, using lstat.  This is necessary in a
+            chroot on at least one system (glibc-2.3.6 + linux 2.6.12), where
+            .., ../.., ../../.., etc. all had the same device number, yet the
+            d_ino values for entries in / did not match those obtained
+            via lstat.  */
+         if (d == NULL && errno == 0 && use_d_ino)
+           {
+             use_d_ino = false;
+             rewinddir (dirstream);
+             d = __readdir (dirstream);
+           }
+
+         if (d == NULL)
+           {
+             if (errno == 0)
+               /* EOF on dirstream, which can mean e.g., that the current
+                  directory has been removed.  */
+               __set_errno (ENOENT);
+             goto lose;
+           }
          if (d->d_name[0] == '.' &&
              (d->d_name[1] == '\0' ||
               (d->d_name[1] == '.' && d->d_name[2] == '\0')))
            continue;
-         if (MATCHING_INO (d, thisino) || mount_point)
+
+         if (use_d_ino)
            {
-             int entry_status;
+             bool match = (MATCHING_INO (d, thisino) || mount_point);
+             if (! match)
+               continue;
+           }
+
+         {
+           int entry_status;
 #ifdef AT_FDCWD
-             entry_status = fstatat (fd, d->d_name, &st, AT_SYMLINK_NOFOLLOW);
+           entry_status = fstatat (fd, d->d_name, &st, AT_SYMLINK_NOFOLLOW);
 #else
-             /* Compute size needed for this file name, or for the file
-                name ".." in the same directory, whichever is larger.
-                Room for ".." might be needed the next time through
-                the outer loop.  */
-             size_t name_alloc = _D_ALLOC_NAMLEN (d);
-             size_t filesize = dotlen + MAX (sizeof "..", name_alloc);
-
-             if (filesize < dotlen)
-               goto memory_exhausted;
-
-             if (dotsize < filesize)
-               {
-                 /* My, what a deep directory tree you have, Grandma.  */
-                 size_t newsize = MAX (filesize, dotsize * 2);
-                 size_t i;
-                 if (newsize < dotsize)
-                   goto memory_exhausted;
-                 if (dotlist != dots)
-                   free (dotlist);
-                 dotlist = malloc (newsize);
-                 if (dotlist == NULL)
-                   goto lose;
-                 dotsize = newsize;
-
-                 i = 0;
-                 do
-                   {
-                     dotlist[i++] = '.';
-                     dotlist[i++] = '.';
-                     dotlist[i++] = '/';
-                   }
-                 while (i < dotlen);
-               }
-
-             strcpy (dotlist + dotlen, d->d_name);
-             entry_status = __lstat (dotlist, &st);
+           /* Compute size needed for this file name, or for the file
+              name ".." in the same directory, whichever is larger.
+              Room for ".." might be needed the next time through
+              the outer loop.  */
+           size_t name_alloc = _D_ALLOC_NAMLEN (d);
+           size_t filesize = dotlen + MAX (sizeof "..", name_alloc);
+
+           if (filesize < dotlen)
+             goto memory_exhausted;
+
+           if (dotsize < filesize)
+             {
+               /* My, what a deep directory tree you have, Grandma.  */
+               size_t newsize = MAX (filesize, dotsize * 2);
+               size_t i;
+               if (newsize < dotsize)
+                 goto memory_exhausted;
+               if (dotlist != dots)
+                 free (dotlist);
+               dotlist = malloc (newsize);
+               if (dotlist == NULL)
+                 goto lose;
+               dotsize = newsize;
+
+               i = 0;
+               do
+                 {
+                   dotlist[i++] = '.';
+                   dotlist[i++] = '.';
+                   dotlist[i++] = '/';
+                 }
+               while (i < dotlen);
+             }
+
+           memcpy (dotlist + dotlen, d->d_name, _D_ALLOC_NAMLEN (d));
+           entry_status = __lstat (dotlist, &st);
 #endif
-             /* We don't fail here if we cannot stat() a directory entry.
-                This can happen when (network) file systems fail.  If this
-                entry is in fact the one we are looking for we will find
-                out soon as we reach the end of the directory without
-                having found anything.  */
-             if (entry_status == 0 && S_ISDIR (st.st_mode)
-                 && st.st_dev == thisdev && st.st_ino == thisino)
-               break;
-           }
-       }
-      if (d == NULL)
-       {
-         if (errno == 0)
-           /* EOF on dirstream, which means that the current directory
-              has been removed.  */
-           __set_errno (ENOENT);
-         goto lose;
+           /* We don't fail here if we cannot stat() a directory entry.
+              This can happen when (network) file systems fail.  If this
+              entry is in fact the one we are looking for we will find
+              out soon as we reach the end of the directory without
+              having found anything.  */
+           if (entry_status == 0 && S_ISDIR (st.st_mode)
+               && st.st_dev == thisdev && st.st_ino == thisino)
+             break;
+         }
        }
-      else
-       {
-         size_t pathroom = pathp - path;
-         size_t namlen = _D_EXACT_NAMLEN (d);
 
-         if (pathroom <= namlen)
+      dirroom = dirp - dir;
+      namlen = _D_EXACT_NAMLEN (d);
+
+      if (dirroom <= namlen)
+       {
+         if (size != 0)
            {
-             if (size != 0)
-               {
-                 __set_errno (ERANGE);
-                 goto lose;
-               }
-             else
-               {
-                 char *tmp;
-                 size_t oldsize = allocated;
-
-                 allocated += MAX (allocated, namlen);
-                 if (allocated < oldsize
-                     || ! (tmp = realloc (path, allocated)))
-                   goto memory_exhausted;
-
-                 /* Move current contents up to the end of the buffer.
-                    This is guaranteed to be non-overlapping.  */
-                 pathp = memcpy (tmp + allocated - (oldsize - pathroom),
-                                 tmp + pathroom,
-                                 oldsize - pathroom);
-                 path = tmp;
-               }
+             __set_errno (ERANGE);
+             goto lose;
+           }
+         else
+           {
+             char *tmp;
+             size_t oldsize = allocated;
+
+             allocated += MAX (allocated, namlen);
+             if (allocated < oldsize
+                 || ! (tmp = realloc (dir, allocated)))
+               goto memory_exhausted;
+
+             /* Move current contents up to the end of the buffer.
+                This is guaranteed to be non-overlapping.  */
+             dirp = memcpy (tmp + allocated - (oldsize - dirroom),
+                            tmp + dirroom,
+                            oldsize - dirroom);
+             dir = tmp;
            }
-         pathp -= namlen;
-         memcpy (pathp, d->d_name, namlen);
-         *--pathp = '/';
        }
+      dirp -= namlen;
+      memcpy (dirp, d->d_name, namlen);
+      *--dirp = '/';
 
       thisdev = dotdev;
       thisino = dotino;
@@ -342,20 +380,27 @@ __getcwd (char *buf, size_t size)
       goto lose;
     }
 
-  if (pathp == &path[allocated - 1])
-    *--pathp = '/';
+  if (dirp == &dir[allocated - 1])
+    *--dirp = '/';
 
 #ifndef AT_FDCWD
   if (dotlist != dots)
     free (dotlist);
 #endif
 
-  memmove (path, pathp, path + allocated - pathp);
+  used = dir + allocated - dirp;
+  memmove (dir, dirp, used);
+
+  if (buf == NULL && size == 0)
+    /* Ensure that the buffer is only as large as necessary.  */
+    buf = realloc (dir, used);
 
-  /* Restore errno on successful return.  */
-  __set_errno (prev_errno);
+  if (buf == NULL)
+    /* Either buf was NULL all along, or `realloc' failed but
+       we still have the original string.  */
+    buf = dir;
 
-  return path;
+  return buf;
 
  memory_exhausted:
   __set_errno (ENOMEM);
@@ -372,7 +417,7 @@ __getcwd (char *buf, size_t size)
       free (dotlist);
 #endif
     if (buf == NULL)
-      free (path);
+      free (dir);
     __set_errno (save);
   }
   return NULL;