Tweak last commit.
[gnulib.git] / lib / getcwd.c
index 3d63f04..2397c08 100644 (file)
@@ -1,10 +1,11 @@
-/* Provide a replacement for the POSIX getcwd function.
-   Copyright (C) 2003, 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
+   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
    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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
-/* written by Jim Meyering */
+#if !_LIBC
+# include <config.h>
+# include <unistd.h>
+# include "dirfd.h"
+#endif
 
-#include <config.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdbool.h>
+#include <stddef.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
+
+#include <dirent.h>
+#ifndef _D_EXACT_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
 
+#include <unistd.h>
 #include <stdlib.h>
 #include <string.h>
-#include <errno.h>
-#include <sys/types.h>
 
-#include "pathmax.h"
-#include "same.h"
+#if _LIBC
+# ifndef mempcpy
+#  define mempcpy __mempcpy
+# endif
+#endif
+
+#include <limits.h>
 
-/* Guess high, because that makes the test below more conservative.
-   But this is a kludge, because we should really use
-   pathconf (".", _PC_NAME_MAX).  But it's probably not worth the cost.  */
-#define KLUDGE_POSIX_NAME_MAX 255
+/* 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
 
-#define MAX_SAFE_LEN (PATH_MAX - 1 - KLUDGE_POSIX_NAME_MAX - 1)
+#ifdef ENAMETOOLONG
+# define is_ENAMETOOLONG(x) ((x) == ENAMETOOLONG)
+#else
+# define is_ENAMETOOLONG(x) 0
+#endif
 
-/* Undefine getcwd here, as near the use as possible, in case any
-   of the files included above define it to rpl_getcwd.  */
-#undef getcwd
+#ifndef MAX
+# define MAX(a, b) ((a) < (b) ? (b) : (a))
+#endif
+#ifndef MIN
+# define MIN(a, b) ((a) < (b) ? (a) : (b))
+#endif
 
-/* Any declaration of getcwd from headers included above has
-   been changed to a declaration of rpl_getcwd.  Declare it here.  */
-extern char *getcwd (char *buf, size_t size);
+#ifndef PATH_MAX
+# ifdef        MAXPATHLEN
+#  define PATH_MAX MAXPATHLEN
+# else
+#  define PATH_MAX 1024
+# endif
+#endif
 
-/* This is a wrapper for getcwd.
-   Some implementations (at least GNU libc 2.3.1 + linux-2.4.20) return
-   non-NULL for a working directory name longer than PATH_MAX, yet the
-   returned string is a strict prefix of the desired directory name.
-   Upon such a failure, free the offending string, set errno to
-   ENAMETOOLONG, and return NULL.
+#if D_INO_IN_DIRENT
+# define MATCHING_INO(dp, ino) ((dp)->d_ino == (ino))
+#else
+# define MATCHING_INO(dp, ino) true
+#endif
 
-   I've heard that this is a Linux kernel bug, and that it has
-   been fixed between 2.4.21-pre3 and 2.4.21-pre4.  */
+#if !_LIBC
+# 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,
+   therefore save some unnecessary recursion in fchdir.c.  */
+#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
+   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,
+   unless SIZE == 0, in which case it is as big as necessary.  */
 
 char *
-rpl_getcwd (char *buf, size_t size)
+__getcwd (char *buf, size_t size)
 {
-  char *cwd = getcwd (buf, size);
+  /* Lengths of big file name components and entire file names, and a
+     deep level of file name nesting.  These numbers are not upper
+     bounds; they are merely large values suitable for initial
+     allocations, designed to be large enough for most real-world
+     uses.  */
+  enum
+    {
+      BIG_FILE_NAME_COMPONENT_LENGTH = 255,
+      BIG_FILE_NAME_LENGTH = MIN (4095, PATH_MAX - 1),
+      DEEP_NESTING = 100
+    };
+
+#if HAVE_OPENAT_SUPPORT
+  int fd = AT_FDCWD;
+  bool fd_needs_closing = false;
+#else
+  char dots[DEEP_NESTING * sizeof ".." + BIG_FILE_NAME_COMPONENT_LENGTH + 1];
+  char *dotlist = dots;
+  size_t dotsize = sizeof dots;
+  size_t dotlen = 0;
+#endif
+  DIR *dirstream = NULL;
+  dev_t rootdev, thisdev;
+  ino_t rootino, thisino;
+  char *dir;
+  register char *dirp;
+  struct stat st;
+  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)
+    {
+      if (buf != NULL)
+       {
+         __set_errno (EINVAL);
+         return NULL;
+       }
+
+      allocated = BIG_FILE_NAME_LENGTH + 1;
+    }
+
+  if (buf == NULL)
+    {
+      dir = malloc (allocated);
+      if (dir == NULL)
+       return NULL;
+    }
+  else
+    dir = buf;
+
+  dirp = dir + allocated;
+  *--dirp = '\0';
+
+  if (__lstat (".", &st) < 0)
+    goto lose;
+  thisdev = st.st_dev;
+  thisino = st.st_ino;
+
+  if (__lstat ("/", &st) < 0)
+    goto lose;
+  rootdev = st.st_dev;
+  rootino = st.st_ino;
+
+  while (!(thisdev == rootdev && thisino == rootino))
+    {
+      struct dirent *d;
+      dev_t dotdev;
+      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.  */
+#if HAVE_OPENAT_SUPPORT
+      fd = openat (fd, "..", O_RDONLY);
+      if (fd < 0)
+       goto lose;
+      fd_needs_closing = true;
+      parent_status = fstat (fd, &st);
+#else
+      dotlist[dotlen++] = '.';
+      dotlist[dotlen++] = '.';
+      dotlist[dotlen] = '\0';
+      parent_status = __lstat (dotlist, &st);
+#endif
+      if (parent_status != 0)
+       goto lose;
 
-  if (cwd == NULL)
-    return NULL;
+      if (dirstream && __closedir (dirstream) != 0)
+       {
+         dirstream = NULL;
+         goto lose;
+       }
 
-  if (strlen (cwd) <= MAX_SAFE_LEN || same_name (cwd, "."))
-    return cwd;
+      /* Figure out if this directory is a mount point.  */
+      dotdev = st.st_dev;
+      dotino = st.st_ino;
+      mount_point = dotdev != thisdev;
 
-  free (cwd);
-  errno = ENAMETOOLONG;
+      /* Search for the last directory.  */
+#if HAVE_OPENAT_SUPPORT
+      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);
+      if (dirstream == NULL)
+       goto lose;
+      dotlist[dotlen++] = '/';
+#endif
+      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);
+             }
+
+           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;
+         }
+       }
+
+      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;
+    }
+
+  if (dirstream && __closedir (dirstream) != 0)
+    {
+      dirstream = NULL;
+      goto lose;
+    }
+
+  if (dirp == &dir[allocated - 1])
+    *--dirp = '/';
+
+#if ! HAVE_OPENAT_SUPPORT
+  if (dotlist != dots)
+    free (dotlist);
+#endif
+
+  used = dir + allocated - dirp;
+  memmove (dir, dirp, used);
+
+  if (size == 0)
+    /* Ensure that the buffer is only as large as necessary.  */
+    buf = realloc (dir, used);
+
+  if (buf == NULL)
+    /* Either buf was NULL all along, or `realloc' failed but
+       we still have the original string.  */
+    buf = dir;
+
+  return buf;
+
+ memory_exhausted:
+  __set_errno (ENOMEM);
+ lose:
+  {
+    int save = errno;
+    if (dirstream)
+      __closedir (dirstream);
+#if HAVE_OPENAT_SUPPORT
+    if (fd_needs_closing)
+      close (fd);
+#else
+    if (dotlist != dots)
+      free (dotlist);
+#endif
+    if (buf == NULL)
+      free (dir);
+    __set_errno (save);
+  }
   return NULL;
 }
+
+#ifdef weak_alias
+weak_alias (__getcwd, getcwd)
+#endif