busybox/libbb/xreadlink.c
Ron Yorston 94eb1c4dc6 libbb: better coreutils compatibility for realpath
Add some tests which coreutils realpath pass but BusyBox realpath
fails (bar one).  Adjust xmalloc_realpath_coreutils() so the tests
pass:

- Expand symbolic links before testing whether the last path component
  exists.

- When the link target is a relative path canonicalize it by passing
  it through xmalloc_realpath_coreutils() as already happens for
  absolute paths.

- Ignore trailing slashes when finding the last path component and
  correctly handle the case where the only slash is at the start of
  the path.  This requires ignoring superfluous leading slashes.

- Undo all changes to the path so error messages from the caller show
  the original filename.

function                                             old     new   delta
xmalloc_realpath_coreutils                           214     313     +99

Signed-off-by: Ron Yorston <rmy@pobox.com>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2021-10-09 01:47:12 +02:00

199 lines
4.5 KiB
C

/* vi: set sw=4 ts=4: */
/*
* xreadlink.c - safe implementation of readlink.
* Returns a NULL on failure.
*
* Licensed under GPLv2, see file LICENSE in this source tree.
*/
#include "libbb.h"
/* Some systems (eg Hurd) do not have MAXSYMLINKS definition,
* set it to some reasonable value if it isn't defined */
#ifndef MAXSYMLINKS
# define MAXSYMLINKS 20
#endif
/*
* NOTE: This function returns a malloced char* that you will have to free
* yourself.
*/
char* FAST_FUNC xmalloc_readlink(const char *path)
{
enum { GROWBY = 80 }; /* how large we will grow strings by */
char *buf = NULL;
int bufsize = 0, readsize = 0;
do {
bufsize += GROWBY;
buf = xrealloc(buf, bufsize);
readsize = readlink(path, buf, bufsize);
if (readsize == -1) {
free(buf);
return NULL;
}
} while (bufsize < readsize + 1);
buf[readsize] = '\0';
return buf;
}
/*
* This routine is not the same as realpath(), which
* canonicalizes the given path completely. This routine only
* follows trailing symlinks until a real file is reached and
* returns its name. If the path ends in a dangling link or if
* the target doesn't exist, the path is returned in any case.
* Intermediate symlinks in the path are not expanded -- only
* those at the tail.
* A malloced char* is returned, which must be freed by the caller.
*/
char* FAST_FUNC xmalloc_follow_symlinks(const char *path)
{
char *buf;
char *lpc;
char *linkpath;
int bufsize;
int looping = MAXSYMLINKS + 1;
buf = xstrdup(path);
goto jump_in;
while (1) {
linkpath = xmalloc_readlink(buf);
if (!linkpath) {
/* not a symlink, or doesn't exist */
if (errno == EINVAL || errno == ENOENT)
return buf;
goto free_buf_ret_null;
}
if (!--looping) {
free(linkpath);
free_buf_ret_null:
free(buf);
return NULL;
}
if (*linkpath != '/') {
bufsize += strlen(linkpath);
buf = xrealloc(buf, bufsize);
lpc = bb_get_last_path_component_strip(buf);
strcpy(lpc, linkpath);
free(linkpath);
} else {
free(buf);
buf = linkpath;
jump_in:
bufsize = strlen(buf) + 1;
}
}
}
char* FAST_FUNC xmalloc_readlink_or_warn(const char *path)
{
char *buf = xmalloc_readlink(path);
if (!buf) {
/* EINVAL => "file: Invalid argument" => puzzled user */
const char *errmsg = "not a symlink";
int err = errno;
if (err != EINVAL)
errmsg = strerror(err);
bb_error_msg("%s: cannot read link: %s", path, errmsg);
}
return buf;
}
char* FAST_FUNC xmalloc_realpath(const char *path)
{
/* NB: uclibc also defines __GLIBC__
* Therefore the test "if glibc, or uclibc >= 0.9.31" looks a bit weird:
*/
#if defined(__GLIBC__) && \
(!defined(__UCLIBC__) || UCLIBC_VERSION >= KERNEL_VERSION(0, 9, 31))
/* glibc provides a non-standard extension */
/* new: POSIX.1-2008 specifies this behavior as well */
return realpath(path, NULL);
#else
char buf[PATH_MAX+1];
/* on error returns NULL (xstrdup(NULL) == NULL) */
return xstrdup(realpath(path, buf));
#endif
}
char* FAST_FUNC xmalloc_realpath_coreutils(char *path)
{
char *buf;
errno = 0;
buf = xmalloc_realpath(path);
/*
* There is one case when "readlink -f" and
* "realpath" from coreutils succeed,
* even though file does not exist, such as:
* /tmp/file_does_not_exist
* (the directory must exist).
*/
if (!buf && errno == ENOENT) {
char *target, c, *last_slash;
size_t i;
target = xmalloc_readlink(path);
if (target) {
/*
* $ ln -s /bin/qwe symlink # note: /bin is a link to /usr/bin
* $ readlink -f symlink
* /usr/bin/qwe
* $ realpath symlink
* /usr/bin/qwe
*/
if (target[0] != '/') {
/*
* $ ln -s target_does_not_exist symlink
* $ readlink -f symlink
* /CURDIR/target_does_not_exist
* $ realpath symlink
* /CURDIR/target_does_not_exist
*/
char *cwd = xrealloc_getcwd_or_warn(NULL);
char *tmp = concat_path_file(cwd, target);
free(cwd);
free(target);
target = tmp;
}
buf = xmalloc_realpath_coreutils(target);
free(target);
return buf;
}
/* ignore leading and trailing slashes */
while (path[0] == '/' && path[1] == '/')
++path;
i = strlen(path) - 1;
while (i > 0 && path[i] == '/')
i--;
c = path[i + 1];
path[i + 1] = '\0';
last_slash = strrchr(path, '/');
if (last_slash == path)
buf = xstrdup(path);
else if (last_slash) {
*last_slash = '\0';
buf = xmalloc_realpath(path);
*last_slash++ = '/';
if (buf) {
unsigned len = strlen(buf);
buf = xrealloc(buf, len + strlen(last_slash) + 2);
buf[len++] = '/';
strcpy(buf + len, last_slash);
}
}
path[i + 1] = c;
}
return buf;
}