diff --git a/src/lib/path-util.c b/src/lib/path-util.c index 5a8dbbf63f..b0f04b2c56 100644 --- a/src/lib/path-util.c +++ b/src/lib/path-util.c @@ -5,6 +5,270 @@ #include "path-util.h" #include +#include +#include + +#define PATH_UTIL_MAX_PATH 8*1024 +#define PATH_UTIL_MAX_SYMLINKS 80 + +static int t_getcwd_alloc(char **dir_r, size_t *asize_r, + const char **error_r) ATTR_NULL(2) +{ + /* @UNSAFE */ + char *dir; + size_t asize = 128; + + dir = t_buffer_get(asize); + while (getcwd(dir, asize) == NULL) { + if (errno != ERANGE) { + *error_r = t_strdup_printf("getcwd() failed: %m"); + return -1; + } + asize = nearest_power(asize+1); + dir = t_buffer_get(asize); + } + if (asize_r != NULL) + *asize_r = asize; + *dir_r = dir; + return 0; +} + +static int path_normalize(const char *path, bool resolve_links, + const char **npath_r, const char **error_r) +{ + /* @UNSAFE */ + unsigned int link_count = 0; + char *npath, *npath_pos; + const char *p; + size_t asize; + + i_assert(path != NULL); + i_assert(npath_r != NULL); + i_assert(error_r != NULL); + + if (path[0] != '/') { + /* relative; initialize npath with current directory */ + if (t_getcwd_alloc(&npath, &asize, error_r) < 0) + return -1; + npath_pos = npath + strlen(npath); + i_assert(npath[0] == '/'); + } else { + /* absolute; initialize npath with root */ + asize = 128; + npath = t_buffer_get(asize); + npath[0] = '/'; + npath_pos = npath + 1; + } + + p = path; + while (*p != '\0') { + struct stat st; + ptrdiff_t seglen; + const char *segend; + + /* skip duplicate shashes */ + while (*p == '/') + p++; + + /* find end of path segment */ + for (segend = p; *segend != '\0' && *segend != '/'; segend++); + + if (segend == p) + break; /* '\0' */ + seglen = segend - p; + if (seglen == 1 && p[0] == '.') { + /* a reference to this segment; nothing to do */ + } else if (seglen == 2 && p[0] == '.' && p[1] == '.') { + /* a reference to parent segment; back up to previous + * slash */ + if (npath_pos > npath + 1) { + if (*(npath_pos-1) == '/') + npath_pos--; + for (; *(npath_pos-1) != '/'; npath_pos--); + } + } else { + /* make sure npath now ends in slash */ + if (*(npath_pos-1) != '/') { + i_assert(npath_pos + 1 < npath + asize); + *(npath_pos++) = '/'; + } + + /* allocate space if necessary */ + if ((npath_pos + seglen + 1) >= (npath + asize)) { + ptrdiff_t npath_offset = npath_pos - npath; + asize = nearest_power(npath_offset + seglen + 2); + npath = t_buffer_reget(npath, asize); + npath_pos = npath + npath_offset; + } + + /* copy segment to normalized path */ + i_assert(p + seglen < npath + asize); + (void)memmove(npath_pos, p, seglen); + npath_pos += seglen; + } + + if (resolve_links) { + /* stat path up to here (segend points to tail) */ + *npath_pos = '\0'; + if (lstat(npath, &st) < 0) { + *error_r = t_strdup_printf("lstat() failed: %m"); + return -1; + } + + if (S_ISLNK (st.st_mode)) { + /* symlink */ + char *npath_link; + size_t lsize = 128, tlen = strlen(segend), espace; + size_t ltlen = (link_count == 0 ? 0 : tlen); + ssize_t ret; + + /* limit link dereferences */ + if (++link_count > PATH_UTIL_MAX_SYMLINKS) { + errno = ELOOP; + *error_r = "Too many symlink dereferences"; + return -1; + } + + /* allocate space for preserving tail of previous symlink and + first attempt at reading symlink with room for the tail + + buffer will look like this: + [npath][0][preserved tail][link buffer][room for tail][0] + */ + espace = ltlen + tlen + 2; + if ((npath_pos + espace + lsize) >= (npath + asize)) { + ptrdiff_t npath_offset = npath_pos - npath; + asize = nearest_power((npath_offset + espace + lsize) + 1); + lsize = asize - (npath_offset + espace); + npath = t_buffer_reget(npath, asize); + npath_pos = npath + npath_offset; + } + + if (ltlen > 0) { + /* preserve tail just after end of npath */ + (void)memmove(npath_pos + 1, segend, ltlen); + } + + /* read the symlink after the preserved tail */ + for (;;) { + npath_link = (npath_pos + 1) + ltlen; + + i_assert(npath_link + lsize < npath + asize); + + /* attempt to read the link */ + if ((ret=readlink(npath, npath_link, lsize)) < 0) { + *error_r = t_strdup_printf("readlink() failed: %m"); + return -1; + } + if ((size_t)ret < lsize) { + /* make static analyzers happy */ + npath_link[ret] = '\0'; + break; + } + + /* sum of new symlink content length + * and path tail length may not + exceed maximum */ + if ((size_t)(ret + tlen) >= PATH_UTIL_MAX_PATH) { + errno = ENAMETOOLONG; + *error_r = "Resulting path is too long"; + return -1; + } + + /* try again with bigger buffer */ + espace = ltlen + tlen + 2; + if ((npath_pos + espace + lsize) >= (npath + asize)) { + ptrdiff_t npath_offset = npath_pos - npath; + asize = nearest_power((npath_offset + espace + lsize) + 1); + lsize = asize - (npath_offset + espace); + npath = t_buffer_reget(npath, asize); + npath_pos = npath + npath_offset; + } + } + + /* add tail of previous path at end of symlink */ + if (ltlen > 0) { + i_assert(npath_pos + 1 + tlen < npath + asize); + (void)memcpy(npath_link + ret, npath_pos + 1, tlen); + } else { + i_assert(segend + tlen < npath + asize); + (void)memcpy(npath_link + ret, segend, tlen); + } + *(npath_link+ret+tlen) = '\0'; + + /* use as new source path */ + path = segend = npath_link; + + if (path[0] == '/') { + /* absolute symlink; start over at root */ + npath_pos = npath + 1; + } else { + /* relative symlink; back up to previous segment */ + if (npath_pos > npath + 1) { + if (*(npath_pos-1) == '/') + npath_pos--; + for (; *(npath_pos-1) != '/'; npath_pos--); + } + } + + } else if (*segend != '\0' && !S_ISDIR (st.st_mode)) { + /* not last segment, but not a directory either */ + errno = ENOTDIR; + *error_r = t_strdup_printf("Not a directory: %s", npath); + return -1; + } + } + + p = segend; + } + + i_assert(npath_pos < npath + asize); + + /* remove any trailing slash */ + if (npath_pos > npath + 1 && *(npath_pos-1) == '/') + npath_pos--; + *npath_pos = '\0'; + + t_buffer_alloc(npath_pos - npath + 1); + *npath_r = npath; + return 0; +} + +int t_normpath(const char *path, const char **npath_r, const char **error_r) +{ + return path_normalize(path, FALSE, npath_r, error_r); +} + +int t_normpath_to(const char *path, const char *root, const char **npath_r, + const char **error_r) +{ + i_assert(path != NULL); + i_assert(root != NULL); + i_assert(npath_r != NULL); + + if (*path == '/') + return t_normpath(path, npath_r, error_r); + + return t_normpath(t_strconcat(root, "/", path, NULL), npath_r, error_r); +} + +int t_realpath(const char *path, const char **npath_r, const char **error_r) +{ + return path_normalize(path, TRUE, npath_r, error_r); +} + +int t_realpath_to(const char *path, const char *root, const char **npath_r, + const char **error_r) +{ + i_assert(path != NULL); + i_assert(root != NULL); + i_assert(npath_r != NULL); + + if (*path == '/') + return t_realpath(path, npath_r, error_r); + + return t_realpath(t_strconcat(root, "/", path, NULL), npath_r, error_r); +} const char *t_abspath(const char *path) { @@ -32,24 +296,9 @@ const char *t_abspath_to(const char *path, const char *root) int t_get_working_dir(const char **dir_r, const char **error_r) { + i_assert(dir_r != NULL); i_assert(error_r != NULL); - - /* @UNSAFE */ - char *dir; - size_t size = 128; - - dir = t_buffer_get(size); - while (getcwd(dir, size) == NULL) { - if (errno != ERANGE) { - *error_r = t_strdup_printf("getcwd() failed: %m"); - return -1; - } - size = nearest_power(size+1); - dir = t_buffer_get(size); - } - t_buffer_alloc(strlen(dir) + 1); - *dir_r = dir; - return 0; + return t_getcwd_alloc((char**)dir_r, NULL, error_r); } int t_readlink(const char *path, const char **dest_r, const char **error_r) diff --git a/src/lib/path-util.h b/src/lib/path-util.h index 68c98acfb4..0ac0a07e00 100644 --- a/src/lib/path-util.h +++ b/src/lib/path-util.h @@ -1,6 +1,40 @@ #ifndef PATH_UTIL_H #define PATH_UTIL_H +/* Returns path as the normalized absolute path, which means that './' + * and '../' components are resolved, and that duplicate and trailing + * slashes are removed. If it's not already the absolute path, it's + * assumed to be relative to the current working directory. + * + * NOTE: Be careful with this function. The resolution of '../' components + * with the parent component as if it were a normal directory is not valid + * if the path contains symbolic links. + * + * Returns 0 on success, and -1 on failure. errno and error_r are set on + * failure, and error_r cannot be NULL. + */ +int t_normpath(const char *path, const char **npath_r, const char **error_r); +/* Like t_normpath(), but path is relative to given root. */ +int t_normpath_to(const char *path, const char *root, const char **npath_r, + const char **error_r); + +/* Returns path as the real normalized absolute path, which means that all + * symbolic links in the path are resolved, that './' and '../' components + * are resolved, and that duplicate and trailing slashes are removed. If it's + * not already the absolute path, it's assumed to be relative to the current + * working directory. + * + * NOTE: This function calls stat() for each path component and more when + * there are symbolic links (just like POSIX realpath()). + * + * Returns 0 on success, and -1 on failure. errno and error_r are set on + * failure, and error_r cannot be NULL. + */ +int t_realpath(const char *path, const char **npath_r, const char **error_r); +/* Like t_realpath(), but path is relative to given root. */ +int t_realpath_to(const char *path, const char *root, const char **npath_r, + const char **error_r); + /* Returns path as absolute path. If it's not already absolute path, * it's assumed to be relative to current working directory. *