getcwd: on Solaris, work better if ancestors are inaccessible
[gnulib.git] / lib / getcwd.c
index 666310a..7d47072 100644 (file)
@@ -1,27 +1,22 @@
-/* Copyright (C) 1991,92,93,94,95,96,97,98,99,2004,2005 Free Software
-   Foundation, Inc.
+/* Copyright (C) 1991-1999, 2004-2010 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
+   This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
-   the Free Software Foundation; either version 2, or (at your option)
-   any later version.
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
 
-   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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
-
-#ifdef HAVE_CONFIG_H
-# include <config.h>
-#endif
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
 #if !_LIBC
-# include "getcwd.h"
+# include <config.h>
+# include <unistd.h>
 #endif
 
 #include <errno.h>
 
 #include <fcntl.h> /* For AT_FDCWD on Solaris 9.  */
 
+/* If this host provides the openat function, then enable
+   code below to make getcwd more efficient and robust.  */
+#ifdef HAVE_OPENAT
+# define HAVE_OPENAT_SUPPORT 1
+#else
+# define HAVE_OPENAT_SUPPORT 0
+#endif
+
 #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)
 # ifndef mempcpy
 #  define mempcpy __mempcpy
 # endif
-#else
-# include "mempcpy.h"
 #endif
 
 #include <limits.h>
 
-#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
@@ -88,7 +67,7 @@
 #endif
 
 #ifndef PATH_MAX
-# ifdef        MAXPATHLEN
+# ifdef MAXPATHLEN
 #  define PATH_MAX MAXPATHLEN
 # else
 #  define PATH_MAX 1024
 #endif
 
 #if !_LIBC
-# define __getcwd getcwd
+# define __getcwd rpl_getcwd
 # define __lstat lstat
 # define __closedir closedir
 # define __opendir opendir
 # define __readdir readdir
 #endif
+
+/* The results of opendir() in this file are not used with dirfd and fchdir,
+   and we do not leak fds to any single-threaded code that could use stdio,
+   therefore save some unnecessary recursion in fchdir.c.
+   FIXME - if the kernel ever adds support for multi-thread safety for
+   avoiding standard fds, then we should use opendir_safer and
+   openat_safer.  */
+#undef opendir
+#undef closedir
 \f
 /* 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
@@ -130,7 +118,7 @@ __getcwd (char *buf, size_t size)
       DEEP_NESTING = 100
     };
 
-#ifdef AT_FDCWD
+#if HAVE_OPENAT_SUPPORT
   int fd = AT_FDCWD;
   bool fd_needs_closing = false;
 #else
@@ -148,26 +136,48 @@ __getcwd (char *buf, size_t size)
   size_t allocated = size;
   size_t used;
 
-#if HAVE_PARTLY_WORKING_GETCWD && !defined AT_FDCWD
-  /* 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.  */
+#if HAVE_RAW_DECL_GETCWD
+  /* 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))
+  if (dir)
     return dir;
+
+  /* Solaris getcwd (NULL, 0) fails with errno == EINVAL, but it has
+     internal magic that lets it work even if an ancestor directory is
+     inaccessible, which is better in many cases.  So in this case try
+     again with a buffer that's almost always big enough.  */
+  if (errno == EINVAL && buf == NULL && size == 0)
+    {
+      char big_buffer[BIG_FILE_NAME_LENGTH + 1];
+      dir = getcwd (big_buffer, sizeof big_buffer);
+      if (dir)
+        return strdup (dir);
+    }
+
+# 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 (errno != ERANGE && errno != ENAMETOOLONG && errno != ENOENT)
+    return NULL;
+# endif
 #endif
 
   if (size == 0)
     {
       if (buf != NULL)
-       {
-         __set_errno (EINVAL);
-         return NULL;
-       }
+        {
+          __set_errno (EINVAL);
+          return NULL;
+        }
 
       allocated = BIG_FILE_NAME_LENGTH + 1;
     }
@@ -176,7 +186,7 @@ __getcwd (char *buf, size_t size)
     {
       dir = malloc (allocated);
       if (dir == NULL)
-       return NULL;
+        return NULL;
     }
   else
     dir = buf;
@@ -201,12 +211,15 @@ __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
+#if HAVE_OPENAT_SUPPORT
       fd = openat (fd, "..", O_RDONLY);
       if (fd < 0)
-       goto lose;
+        goto lose;
       fd_needs_closing = true;
       parent_status = fstat (fd, &st);
 #else
@@ -216,13 +229,13 @@ __getcwd (char *buf, size_t size)
       parent_status = __lstat (dotlist, &st);
 #endif
       if (parent_status != 0)
-       goto lose;
+        goto lose;
 
       if (dirstream && __closedir (dirstream) != 0)
-       {
-         dirstream = NULL;
-         goto lose;
-       }
+        {
+          dirstream = NULL;
+          goto lose;
+        }
 
       /* Figure out if this directory is a mount point.  */
       dotdev = st.st_dev;
@@ -230,121 +243,144 @@ __getcwd (char *buf, size_t size)
       mount_point = dotdev != thisdev;
 
       /* Search for the last directory.  */
-#ifdef AT_FDCWD
+#if HAVE_OPENAT_SUPPORT
       dirstream = fdopendir (fd);
       if (dirstream == NULL)
-       goto lose;
+        goto lose;
+      /* Reset fd.  It may have been closed by fdopendir.  */
+      fd = dirfd (dirstream);
       fd_needs_closing = false;
 #else
       dirstream = __opendir (dotlist);
       if (dirstream == NULL)
-       goto lose;
+        goto lose;
       dotlist[dotlen++] = '/';
 #endif
-      /* Clear errno to distinguish EOF from error if readdir returns
-        NULL.  */
-      __set_errno (0);
-      while ((d = __readdir (dirstream)) != NULL)
-       {
-         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)
-           {
-             int entry_status;
-#ifdef AT_FDCWD
-             entry_status = fstatat (fd, d->d_name, &st, AT_SYMLINK_NOFOLLOW);
+      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 (use_d_ino)
+            {
+              bool match = (MATCHING_INO (d, thisino) || mount_point);
+              if (! match)
+                continue;
+            }
+
+          {
+            int entry_status;
+#if HAVE_OPENAT_SUPPORT
+            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;
-       }
-      else
-       {
-         size_t dirroom = dirp - dir;
-         size_t namlen = _D_EXACT_NAMLEN (d);
-
-         if (dirroom <= namlen)
-           {
-             if (size != 0)
-               {
-                 __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;
-               }
-           }
-         dirp -= namlen;
-         memcpy (dirp, d->d_name, namlen);
-         *--dirp = '/';
-       }
+            /* 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;
+          }
+        }
+
+      dirroom = dirp - dir;
+      namlen = _D_EXACT_NAMLEN (d);
+
+      if (dirroom <= namlen)
+        {
+          if (size != 0)
+            {
+              __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;
+            }
+        }
+      dirp -= namlen;
+      memcpy (dirp, d->d_name, namlen);
+      *--dirp = '/';
 
       thisdev = dotdev;
       thisino = dotino;
@@ -359,7 +395,7 @@ __getcwd (char *buf, size_t size)
   if (dirp == &dir[allocated - 1])
     *--dirp = '/';
 
-#ifndef AT_FDCWD
+#if ! HAVE_OPENAT_SUPPORT
   if (dotlist != dots)
     free (dotlist);
 #endif
@@ -367,7 +403,7 @@ __getcwd (char *buf, size_t size)
   used = dir + allocated - dirp;
   memmove (dir, dirp, used);
 
-  if (buf == NULL && size == 0)
+  if (size == 0)
     /* Ensure that the buffer is only as large as necessary.  */
     buf = realloc (dir, used);
 
@@ -385,7 +421,7 @@ __getcwd (char *buf, size_t size)
     int save = errno;
     if (dirstream)
       __closedir (dirstream);
-#ifdef AT_FDCWD
+#if HAVE_OPENAT_SUPPORT
     if (fd_needs_closing)
       close (fd);
 #else