summaryrefslogtreecommitdiff
path: root/lib/chdir-long.c
diff options
context:
space:
mode:
authorJim Meyering <jim@meyering.net>2004-11-30 14:35:33 +0000
committerJim Meyering <jim@meyering.net>2004-11-30 14:35:33 +0000
commit036728c2e887e11e28e2a92f980eaa94afe900ed (patch)
tree5d2323854e4f08bbb3c4acb84f8353c82416f7df /lib/chdir-long.c
parent2a807da9726e79e2cfb1760919f8e38626a18042 (diff)
downloadcoreutils-036728c2e887e11e28e2a92f980eaa94afe900ed.tar.xz
Renamed from chdir.c.
Diffstat (limited to 'lib/chdir-long.c')
-rw-r--r--lib/chdir-long.c342
1 files changed, 342 insertions, 0 deletions
diff --git a/lib/chdir-long.c b/lib/chdir-long.c
new file mode 100644
index 000000000..cb9b7d725
--- /dev/null
+++ b/lib/chdir-long.c
@@ -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:
+*/