1
0
mirror of git://git.sv.gnu.org/coreutils.git synced 2026-04-02 02:04:35 +02:00
Files
coreutils/lib/chdir-long.c
2004-11-30 14:35:33 +00:00

343 lines
8.1 KiB
C

/* 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:
*/