summaryrefslogtreecommitdiffstats
path: root/lib/chdir-long.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chdir-long.c')
-rw-r--r--lib/chdir-long.c264
1 files changed, 264 insertions, 0 deletions
diff --git a/lib/chdir-long.c b/lib/chdir-long.c
new file mode 100644
index 0000000..f4efb20
--- /dev/null
+++ b/lib/chdir-long.c
@@ -0,0 +1,264 @@
+/* provide a chdir function that tries not to fail due to ENAMETOOLONG
+ Copyright (C) 2004-2022 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 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, see <https://www.gnu.org/licenses/>. */
+
+/* written by Jim Meyering */
+
+#include <config.h>
+
+#include "chdir-long.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "assure.h"
+
+#ifndef PATH_MAX
+# error "compile this file only if your system defines PATH_MAX"
+#endif
+
+/* The results of openat() in this file are not leaked to any
+ single-threaded code that could use stdio.
+ FIXME - if the kernel ever adds support for multi-thread safety for
+ avoiding standard fds, then we should use openat_safer. */
+
+struct cd_buf
+{
+ int fd;
+};
+
+static void
+cdb_init (struct cd_buf *cdb)
+{
+ cdb->fd = AT_FDCWD;
+}
+
+static int
+cdb_fchdir (struct cd_buf const *cdb)
+{
+ return fchdir (cdb->fd);
+}
+
+static void
+cdb_free (struct cd_buf const *cdb)
+{
+ if (0 <= cdb->fd)
+ {
+ bool close_fail = close (cdb->fd);
+ assure (! close_fail);
+ }
+}
+
+/* Given a file descriptor of an open directory (or AT_FDCWD), CDB->fd,
+ try to open the CDB->fd-relative directory, DIR. If the open succeeds,
+ update CDB->fd with the resulting descriptor, close the incoming file
+ descriptor, and return zero. Upon failure, return -1 and set errno. */
+static int
+cdb_advance_fd (struct cd_buf *cdb, char const *dir)
+{
+ int new_fd = openat (cdb->fd, dir,
+ O_SEARCH | O_DIRECTORY | O_NOCTTY | O_NONBLOCK);
+ if (new_fd < 0)
+ return -1;
+
+ cdb_free (cdb);
+ cdb->fd = new_fd;
+
+ return 0;
+}
+
+/* Return a pointer to the first non-slash in S. */
+static char * _GL_ATTRIBUTE_PURE
+find_non_slash (char const *s)
+{
+ size_t n_slash = strspn (s, "/");
+ return (char *) s + n_slash;
+}
+
+/* This is a function much like chdir, but without the PATH_MAX limitation
+ on the length of the directory name. A significant difference is that
+ it must be able to modify (albeit only temporarily) the directory
+ name. It handles an arbitrarily long directory name by operating
+ on 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 save 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
+ has length PATH_MAX or greater on systems that define PATH_MAX). */
+
+int
+chdir_long (char *dir)
+{
+ int e = chdir (dir);
+ if (e == 0 || errno != ENAMETOOLONG)
+ return e;
+
+ {
+ size_t len = strlen (dir);
+ char *dir_end = dir + len;
+ struct cd_buf cdb;
+ size_t n_leading_slash;
+
+ cdb_init (&cdb);
+
+ /* If DIR is the empty string, then the chdir above
+ must have failed and set errno to ENOENT. */
+ assure (0 < len);
+ assure (PATH_MAX <= len);
+
+ /* Count leading slashes. */
+ n_leading_slash = strspn (dir, "/");
+
+ /* Handle any leading slashes as well as any name that matches
+ the regular expression, m!^//hostname[/]*! . Handling this
+ prefix separately usually results in a single additional
+ cdb_advance_fd call, but it's worthwhile, since it makes the
+ code in the following loop cleaner. */
+ if (n_leading_slash == 2)
+ {
+ int err;
+ /* Find next slash.
+ We already know that dir[2] is neither a slash nor '\0'. */
+ char *slash = memchr (dir + 3, '/', dir_end - (dir + 3));
+ if (slash == NULL)
+ {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ *slash = '\0';
+ err = cdb_advance_fd (&cdb, dir);
+ *slash = '/';
+ if (err != 0)
+ goto Fail;
+ dir = find_non_slash (slash + 1);
+ }
+ else if (n_leading_slash)
+ {
+ if (cdb_advance_fd (&cdb, "/") != 0)
+ goto Fail;
+ dir += n_leading_slash;
+ }
+
+ assure (*dir != '/');
+ assure (dir <= dir_end);
+
+ while (PATH_MAX <= dir_end - dir)
+ {
+ int err;
+ /* Find a slash that is PATH_MAX or fewer bytes away from dir.
+ I.e. see if there is a slash that will give us a name of
+ length PATH_MAX-1 or less. */
+ char *slash = memrchr (dir, '/', PATH_MAX);
+ if (slash == NULL)
+ {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ *slash = '\0';
+ assure (slash - dir < PATH_MAX);
+ err = cdb_advance_fd (&cdb, dir);
+ *slash = '/';
+ if (err != 0)
+ goto Fail;
+
+ dir = find_non_slash (slash + 1);
+ }
+
+ if (dir < dir_end)
+ {
+ if (cdb_advance_fd (&cdb, dir) != 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 "closeout.h"
+# include "error.h"
+
+int
+main (int argc, char *argv[])
+{
+ char *line = NULL;
+ size_t n = 0;
+ int len;
+
+ 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);
+
+ if (argc <= 1)
+ {
+ /* 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);
+ }
+
+ fclose (stdin);
+ fclose (stderr);
+
+ exit (EXIT_SUCCESS);
+}
+#endif
+
+/*
+Local Variables:
+compile-command: "gcc -DTEST_CHDIR=1 -g -O -W -Wall chdir-long.c libcoreutils.a"
+End:
+*/