]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
Renamed from chdir.c.
authorJim Meyering <jim@meyering.net>
Tue, 30 Nov 2004 14:35:33 +0000 (14:35 +0000)
committerJim Meyering <jim@meyering.net>
Tue, 30 Nov 2004 14:35:33 +0000 (14:35 +0000)
lib/chdir-long.c [new file with mode: 0644]

diff --git a/lib/chdir-long.c b/lib/chdir-long.c
new file mode 100644 (file)
index 0000000..cb9b7d7
--- /dev/null
@@ -0,0 +1,342 @@
+/* provide a chdir function that tries not to fail due to ENAMETOOLONG
+   Copyright (C) 2004 Free Software Foundation, Inc.
+
+   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.
+
+   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
+
+/* written by Jim Meyering */
+
+#include <config.h>
+
+#include "chdir-long.h"
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "mempcpy.h"
+#include "openat.h"
+
+#ifndef O_DIRECTORY
+# define O_DIRECTORY 0
+#endif
+
+#ifndef MIN
+# define MIN(a, b) ((a) < (b) ? (a) : (b))
+#endif
+
+#ifndef PATH_MAX
+# ifdef        MAXPATHLEN
+#  define PATH_MAX MAXPATHLEN
+# else
+#  error "use this module only if your system defines PATH_MAX"
+# endif
+#endif
+
+/* FIXME: this use of `MIN' is our sole concession to arbitrary limitations.
+   If, for some system, PATH_MAX is larger than 8191 and you call
+   chdir_long with a directory name that is longer than PATH_MAX,
+   yet that contains a single component that is more than 8191 bytes
+   long, then this function will fail.  */
+#define MAX_COMPONENT_LENGTH MIN (PATH_MAX - 1, 8 * 1024)
+
+struct cd_buf
+{
+  /* FIXME maybe allocate this via malloc, rather than using the stack.
+     But that would be the sole use of malloc.  Is it worth it to
+     let chdir_long fail due to a low-memory condition?
+     But when using malloc, and assuming we remove the `concession'
+     above, we'll still have to avoid allocating 2^31 bytes on
+     systems that define PATH_MAX to very large number.
+     Ideally, we'd allocate enough to deal with most names, and
+     dynamically increase the buffer size only necessary.  */
+  char buffer[MAX_COMPONENT_LENGTH + 1];
+  char *avail;
+  int fd;
+};
+
+/* Like memchr, but return the number of bytes from MEM
+   to the first occurrence of C thereafter.  Search only
+   LEN bytes.  Return LEN if C is not found.  */
+static inline size_t
+memchrcspn (char const *mem, int c, size_t len)
+{
+  char const *found = memchr (mem, c, len);
+  if (!found)
+    return len;
+
+  len = found - mem;
+  return len;
+}
+
+static void
+cdb_init (struct cd_buf *cdb)
+{
+  cdb->avail = cdb->buffer;
+  cdb->fd = AT_FDCWD;
+}
+
+static inline bool
+cdb_empty (struct cd_buf const *cdb)
+{
+  return cdb->avail == cdb->buffer;
+}
+
+static inline int
+cdb_fchdir (struct cd_buf const *cdb)
+{
+  return fchdir (cdb->fd);
+}
+
+static int
+cdb_advance_fd (struct cd_buf *cdb, char const *dir)
+{
+  int new_fd = openat (cdb->fd, dir, O_RDONLY | O_DIRECTORY);
+  if (new_fd < 0)
+    {
+      new_fd = openat (cdb->fd, dir, O_WRONLY | O_DIRECTORY);
+      if (new_fd < 0)
+       return -1;
+    }
+
+  if (cdb->fd != AT_FDCWD)
+    close (cdb->fd);
+  cdb->fd = new_fd;
+
+  return 0;
+}
+
+static int
+cdb_flush (struct cd_buf *cdb)
+{
+  if (cdb_empty (cdb))
+    return 0;
+
+  cdb->avail[0] = '\0';
+  if (cdb_advance_fd (cdb, cdb->buffer) != 0)
+    return -1;
+
+  cdb->avail = cdb->buffer;
+
+  return 0;
+}
+
+static void
+cdb_free (struct cd_buf *cdb)
+{
+  if (0 <= cdb->fd && close (cdb->fd) != 0)
+    abort ();
+}
+
+static int
+cdb_append (struct cd_buf *cdb, char const *s, size_t len)
+{
+  char const *end = cdb->buffer + sizeof cdb->buffer;
+
+  /* Insert a slash separator if there is a preceding byte
+     and it's not a slash.  */
+  bool need_slash = (cdb->buffer < cdb->avail && cdb->avail[-1] != '/');
+  size_t n_free;
+
+  if (sizeof cdb->buffer < len + 1)
+    {
+      /* This single component is too long.  */
+      errno = ENAMETOOLONG;
+      return -1;
+    }
+
+  /* See if there's enough room for the `/', the new component and
+     a trailing NUL.  */
+  n_free = end - cdb->avail;
+  if (n_free < need_slash + len + 1)
+    {
+      if (cdb_flush (cdb) != 0)
+       return -1;
+      need_slash = false;
+    }
+
+  if (need_slash)
+    *(cdb->avail)++ = '/';
+
+  cdb->avail = mempcpy (cdb->avail, s, len);
+  return 0;
+}
+
+/* This is a wrapper around chdir that works even on PATH_MAX-limited
+   systems.  It handles an arbitrarily long directory name by extracting
+   and processing manageable portions of the name.  On systems without
+   the openat syscall, this means changing the working directory to
+   more and more `distant' points along the long directory name and
+   then restoring the working directory.
+   If any of those attempts to change or restore the working directory
+   fails, this function exits nonzero.
+
+   Note that this function may still fail with errno == ENAMETOOLONG,
+   but only if the specified directory name contains a component that
+   is long enough to provoke such a failure all by itself (e.g. if the
+   component is longer than PATH_MAX on systems that define PATH_MAX).  */
+
+int
+chdir_long (char const *dir)
+{
+  int e = chdir (dir);
+  if (e == 0 || errno != ENAMETOOLONG)
+    return e;
+
+  {
+    size_t len = strlen (dir);
+    char const *dir_end = dir + len;
+    char const *d;
+    struct cd_buf cdb;
+
+    cdb_init (&cdb);
+
+    /* If DIR is the empty string, then the chdir above
+       must have failed and set errno to ENOENT.  */
+    assert (0 < len);
+
+    if (*dir == '/')
+      {
+       /* Names starting with exactly two slashes followed by at least
+          one non-slash are special --
+          for example, in some environments //Hostname/file may
+          denote a file on a different host.
+          Preserve those two leading slashes.  Treat all other
+          sequences of slashes like a single one.  */
+       if (3 <= len && dir[1] == '/' && dir[2] != '/')
+         {
+           size_t name_len = 1 + strcspn (dir + 3, "/");
+           if (cdb_append (&cdb, dir, 2 + name_len) != 0)
+             goto Fail;
+           /* Advance D to next slash or to end of string. */
+           d = dir + 2 + name_len;
+           assert (*d == '/' || *d == '\0');
+         }
+       else
+         {
+           if (cdb_append (&cdb, "/", 1) != 0)
+             goto Fail;
+           d = dir + 1;
+         }
+      }
+    else
+      {
+       d = dir;
+      }
+
+    while (1)
+      {
+       /* Skip any slashes to find start of next component --
+          or the end of DIR. */
+       char const *start = d + strspn (d, "/");
+       if (*start == '\0')
+         {
+           if (cdb_flush (&cdb) != 0)
+             goto Fail;
+           break;
+         }
+       /* If the remaining portion is no longer than PATH_MAX, then
+          flush anything that is buffered and do the rest in one chunk.  */
+       if (dir_end - start <= PATH_MAX)
+         {
+           if (cdb_flush (&cdb) != 0
+               || cdb_advance_fd (&cdb, start) != 0)
+             goto Fail;
+           break;
+         }
+
+       len = memchrcspn (start, '/', dir_end - start);
+       assert (len == strcspn (start, "/"));
+       d = start + len;
+       if (cdb_append (&cdb, start, len) != 0)
+         goto Fail;
+      }
+
+    if (cdb_fchdir (&cdb) != 0)
+      goto Fail;
+
+    cdb_free (&cdb);
+    return 0;
+
+   Fail:
+    {
+      int saved_errno = errno;
+      cdb_free (&cdb);
+      errno = saved_errno;
+      return -1;
+    }
+  }
+}
+
+#if TEST_CHDIR
+
+# include <stdio.h>
+# include "closeout.h"
+# include "error.h"
+
+char *program_name;
+
+int
+main (int argc, char *argv[])
+{
+  char *line = NULL;
+  size_t n = 0;
+  int len;
+
+  program_name = argv[0];
+  atexit (close_stdout);
+
+  len = getline (&line, &n, stdin);
+  if (len < 0)
+    {
+      int saved_errno = errno;
+      if (feof (stdin))
+       exit (0);
+
+      error (EXIT_FAILURE, saved_errno,
+            "reading standard input");
+    }
+  else if (len == 0)
+    exit (0);
+
+  if (line[len-1] == '\n')
+    line[len-1] = '\0';
+
+  if (chdir_long (line) != 0)
+    error (EXIT_FAILURE, errno,
+          "chdir_long failed: %s", line);
+
+  {
+    /* Using `pwd' here makes sense only if it is a robust implementation,
+       like the one in coreutils after the 2004-04-19 changes.  */
+    char const *cmd = "pwd";
+    execlp (cmd, (char *) NULL);
+    error (EXIT_FAILURE, errno, "%s", cmd);
+  }
+
+  /* not reached */
+  abort ();
+}
+#endif
+
+/*
+Local Variables:
+compile-command: "gcc -DTEST_CHDIR=1 -DHAVE_CONFIG_H -I.. -g -O -W -Wall chdir-long.c libfetish.a"
+End:
+*/