From 5427ab41c897ad668151a36533db41d5a609f9ee Mon Sep 17 00:00:00 2001 From: Erik van der Kouwe Date: Fri, 4 Dec 2009 07:52:22 +0000 Subject: [PATCH] Add realpath function --- include/stdlib.h | 2 + lib/other/Makefile.in | 1 + lib/other/realpath.c | 221 +++++++++++++++++++++++++++++++++ man/man3/realpath.3 | 20 +++ test/Makefile | 3 +- test/run | 4 +- test/test43.c | 283 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 531 insertions(+), 3 deletions(-) create mode 100644 lib/other/realpath.c create mode 100644 man/man3/realpath.3 create mode 100644 test/test43.c diff --git a/include/stdlib.h b/include/stdlib.h index d170623ee..3b3adab96 100644 --- a/include/stdlib.h +++ b/include/stdlib.h @@ -69,6 +69,8 @@ _PROTOTYPE( int mkstemp, (char *_fmt) ); _PROTOTYPE( char *initstate, (unsigned _seed, char *_state, size_t _size) ); _PROTOTYPE( long random, (void) ); +_PROTOTYPE( char *realpath, (const char *file_name, + char *resolved_name) ); _PROTOTYPE( char *setstate, (const char *state) ); _PROTOTYPE( void srandom, (unsigned seed) ); _PROTOTYPE( int putenv, (char *string) ); diff --git a/lib/other/Makefile.in b/lib/other/Makefile.in index 344a34a81..6d84c8570 100644 --- a/lib/other/Makefile.in +++ b/lib/other/Makefile.in @@ -75,6 +75,7 @@ libc_FILES=" \ putenv.c \ putw.c \ random.c \ + realpath.c \ rindex.c \ setenv.c \ setgroups.c \ diff --git a/lib/other/realpath.c b/lib/other/realpath.c new file mode 100644 index 000000000..a2c25c1f0 --- /dev/null +++ b/lib/other/realpath.c @@ -0,0 +1,221 @@ +/* realpath() - resolve absolute path Author: Erik van der Kouwe + * 4 December 2009 + * + * Based on this specification: + * http://www.opengroup.org/onlinepubs/000095399/functions/realpath.html + */ + +#include +#include +#include +#include +#include + +static char *append_path_component(char *path, const char *component, + size_t component_length); +static char *process_path_component(const char *component, + size_t component_length, char *resolved_name, int last_part, int max_depth); +static char *realpath_recurse(const char *file_name, char *resolved_name, + int max_depth); +static char *remove_last_path_component(char *path); + +static char *append_path_component(char *path, const char *component, + size_t component_length) +{ + size_t path_length, slash_length; + + /* insert or remove a slash? */ + path_length = strlen(path); + slash_length = + ((path[path_length - 1] == '/') ? 0 : 1) + + ((component[0] == '/') ? 0 : 1) - 1; + + /* check whether this fits */ + if (path_length + slash_length + component_length >= PATH_MAX) + { + errno = ENAMETOOLONG; + return NULL; + } + + /* insert slash if needed */ + if (slash_length > 0) + path[path_length] = '/'; + + /* copy the bytes */ + memcpy(path + path_length + slash_length, component, component_length); + path[path_length + slash_length + component_length] = 0; + + return path; +} + +static char *process_path_component(const char *component, + size_t component_length, char *resolved_name, int last_part, int max_depth) +{ + char readlink_buffer[PATH_MAX + 1]; + ssize_t readlink_buffer_length; + struct stat stat_buffer; + + /* handle zero-length components */ + if (!component_length) + { + if (last_part) + return resolved_name; + else + { + errno = ENOENT; + return NULL; + } + } + + /* ignore current directory components */ + if (component_length == 1 && component[0] == '.') + return resolved_name; + + /* process parent directory components */ + if (component_length == 2 && component[0] == '.' && component[1] == '.') + return remove_last_path_component(resolved_name); + + /* not a special case, so just add the component */ + if (!append_path_component(resolved_name, component, component_length)) + return NULL; + + /* stat partially resolved file */ + if (lstat(resolved_name, &stat_buffer) < 0) + { + if (last_part && errno == ENOENT) + return resolved_name; + else + return NULL; + } + + if (S_ISLNK(stat_buffer.st_mode)) + { + /* resolve symbolic link */ + readlink_buffer_length = readlink(resolved_name, readlink_buffer, + sizeof(readlink_buffer) - 1); + if (readlink_buffer_length < 0) + return NULL; + + readlink_buffer[readlink_buffer_length] = 0; + + /* recurse to resolve path in link */ + remove_last_path_component(resolved_name); + if (!realpath_recurse(readlink_buffer, resolved_name, max_depth - 1)) + return NULL; + + /* stat symlink target */ + if (lstat(resolved_name, &stat_buffer) < 0) + { + if (last_part && errno == ENOENT) + return resolved_name; + else + return NULL; + } + } + + /* non-directories may appear only as the last component */ + if (!last_part && !S_ISDIR(stat_buffer.st_mode)) + { + errno = ENOTDIR; + return NULL; + } + + return resolved_name; +} + +static char *realpath_recurse(const char *file_name, char *resolved_name, + int max_depth) +{ + const char *file_name_component; + + /* avoid infinite recursion */ + if (max_depth <= 0) + { + errno = ELOOP; + return NULL; + } + + /* relative to root or to current? */ + if (file_name[0] == '/') + { + /* relative to root */ + resolved_name[0] = '/'; + resolved_name[1] = '\0'; + file_name++; + } + + /* process the path component by component */ + while (*file_name) + { + /* extract a slash-delimited component */ + file_name_component = file_name; + while (*file_name && *file_name != '/') + file_name++; + + /* check length of component */ + if (file_name - file_name_component > PATH_MAX) + { + errno = ENAMETOOLONG; + return NULL; + } + + /* add the component to the current result */ + if (!process_path_component( + file_name_component, + file_name - file_name_component, + resolved_name, + !*file_name, + max_depth)) + return NULL; + + /* skip the slash */ + if (*file_name == '/') + file_name++; + } + + return resolved_name; +} + +static char *remove_last_path_component(char *path) +{ + char *current, *slash; + + /* find the last slash */ + slash = NULL; + for (current = path; *current; current++) + if (*current == '/') + slash = current; + + /* truncate after the last slash, but do not remove the root */ + if (slash > path) + *slash = 0; + else if (slash == path) + slash[1] = 0; + + return path; +} + +char *realpath(const char *file_name, char *resolved_name) +{ + /* check parameters */ + if (!file_name || !resolved_name) + { + errno = EINVAL; + return NULL; + } + + if (strlen(file_name) > PATH_MAX) + { + errno = ENAMETOOLONG; + return NULL; + } + + /* basis to resolve against: root or CWD */ + if (file_name[0] == '/') + *resolved_name = 0; + else if (!getcwd(resolved_name, PATH_MAX)) + return NULL; + + /* do the actual work */ + return realpath_recurse(file_name, resolved_name, SYMLOOP_MAX); +} diff --git a/man/man3/realpath.3 b/man/man3/realpath.3 new file mode 100644 index 000000000..d5cebbf13 --- /dev/null +++ b/man/man3/realpath.3 @@ -0,0 +1,20 @@ +.TH REALPATH 3 "December 3, 2009" +.UC 4 +.SH NAME +realpath \- resolve a pathname +.SH SYNOPSIS +.nf +.ft B +#include + +char *realpath(const char *\fIfile_name\fP, char *\fIresolved_name\fP); +.fi +.SH DESCRIPTION +realpath finds an absolute path to \fIfile_name\fP which does not +contain . and .. components or symbolic links. The absolute path is stored +in \fIresolved_name\fP, which is expected to provide storage for at least +MAX_PATH bytes. +.SH "RETURN VALUE +If the function succeeds, a pointer to \fIresolved_name\fP is returned. +If the function fails, NULL is returned and errno is set to indicate the +cause of the failure. diff --git a/test/Makefile b/test/Makefile index 6262d8d89..eaab7e3f2 100644 --- a/test/Makefile +++ b/test/Makefile @@ -8,7 +8,7 @@ OBJ= test1 test2 test3 test4 test5 test6 test7 test8 test9 \ test21 test22 test23 test25 test26 test27 test28 test29 \ test30 test31 test32 test34 test35 test36 test37 test38 \ test39 t10a t11a t11b test40 t40a t40b t40c t40d t40e t40f test41 \ - test42 + test42 test43 BIGOBJ= test20 test24 ROOTOBJ= test11 test33 @@ -83,3 +83,4 @@ t40e: t40e.c t40f: t40f.c test41: test41.c test42: test42.c +test43: test43.c diff --git a/test/run b/test/run index 7fba9a7fc..4e9297df7 100755 --- a/test/run +++ b/test/run @@ -12,13 +12,13 @@ badones= # list of tests that failed # Print test welcome message clr -echo "Running POSIX compliance test suite. There are 44 tests in total." +echo "Running POSIX compliance test suite. There are 45 tests in total." echo " " # Run all the tests, keeping track of who failed. for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \ 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 \ - 41 42 sh1.sh sh2.sh + 41 42 43 sh1.sh sh2.sh do total=`expr $total + 1` FAIL=0 if [ $USER = root -a \( $i = 11 -o $i = 33 \) ] diff --git a/test/test43.c b/test/test43.c new file mode 100644 index 000000000..35e07fdd8 --- /dev/null +++ b/test/test43.c @@ -0,0 +1,283 @@ +/* Tests for MINIX3 realpath(3) - by Erik van der Kouwe */ +#define _POSIX_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_ERROR 4 + +static int errct = 0; +static const char *executable, *subtest; + +#define ERR (e(__LINE__)) + +static void e(int n) +{ + printf("File %s, error line %d, errno %d: %s\n", + subtest, n, errno, strerror(errno)); + + if (errct++ > MAX_ERROR) + { + printf("Too many errors; test aborted\n"); + exit(1); + } +} + +static void quit(void) +{ + if (errct == 0) + { + printf("ok\n"); + exit(0); + } + else + { + printf("%d errors\n", errct); + exit(1); + } +} + +static char *remove_last_path_component(char *path) +{ + char *current, *last; + + assert(path); + + /* find last slash */ + last = NULL; + for (current = path; *current; current++) + if (*current == '/') + last = current; + + /* check path component */ + if (last) + { + if (strcmp(last + 1, ".") == 0) ERR; + if (strcmp(last + 1, "..") == 0) ERR; + } + + /* if only root path slash, we are done */ + if (last <= path) + return NULL; + + /* chop off last path component */ + *last = 0; + return path; +} + +static void check_realpath(const char *path, int expected_errno) +{ + char buffer[PATH_MAX + 1], *resolved_path; + struct stat statbuf[2]; + + assert(path); + + /* run realpath */ + subtest = path; + resolved_path = realpath(path, buffer); + + /* do we get errors when expected? */ + if (expected_errno) + { + if (errno != expected_errno) ERR; + if (resolved_path) ERR; + subtest = NULL; + return; + } + + /* do we get success when expected? */ + if (!resolved_path) + { + ERR; + subtest = NULL; + return; + } + errno = 0; + + /* do the paths point to the same file? */ + if (stat(path, &statbuf[0]) < 0) { ERR; return; } + if (stat(resolved_path, &statbuf[1]) < 0) { ERR; return; } + if (statbuf[0].st_dev != statbuf[1].st_dev) ERR; + if (statbuf[0].st_ino != statbuf[1].st_ino) ERR; + + /* is the path absolute? */ + if (resolved_path[0] != '/') ERR; + + /* is each path element allowable? */ + while (remove_last_path_component(resolved_path)) + { + /* not a symlink? */ + if (lstat(resolved_path, &statbuf[1]) < 0) { ERR; return; } + if ((statbuf[1].st_mode & S_IFMT) != S_IFDIR) ERR; + } + subtest = NULL; +} + +static void check_realpath_step_by_step(const char *path, int expected_errno) +{ + char buffer[PATH_MAX + 1]; + const char *path_current; + + assert(path); + assert(strlen(path) < sizeof(buffer)); + + /* check the absolute path */ + check_realpath(path, expected_errno); + + /* try with different CWDs */ + for (path_current = path; *path_current; path_current++) + if (path_current[0] == '/' && path_current[1]) + { + /* set CWD */ + memcpy(buffer, path, path_current - path + 1); + buffer[path_current - path + 1] = 0; + if (chdir(buffer) < 0) { ERR; continue; } + + /* perform test */ + check_realpath(path_current + 1, expected_errno); + } +} + +static char *pathncat(char *buffer, size_t size, const char *path1, const char *path2) +{ + size_t len1, len2, lenslash; + + assert(buffer); + assert(path1); + assert(path2); + + /* check whether it fits */ + len1 = strlen(path1); + len2 = strlen(path2); + lenslash = (len1 > 0 && path1[len1 - 1] == '/') ? 0 : 1; + if (len1 >= size || /* check individual components to avoid overflow */ + len2 >= size || + len1 + len2 + lenslash >= size) + return NULL; + + /* perform the copy */ + memcpy(buffer, path1, len1); + if (lenslash) + buffer[len1] = '/'; + + memcpy(buffer + len1 + lenslash, path2, len2 + 1); + return buffer; +} + +static void check_realpath_recurse(const char *path, int depth) +{ + DIR *dir; + struct dirent *dirent; + char pathsub[PATH_MAX + 1]; + + /* check with the path itself */ + check_realpath_step_by_step(path, 0); + + /* don't go too deep */ + if (depth < 1) + return; + + /* loop through subdirectories (including . and ..) */ + if (!(dir = opendir(path))) + { + if (errno != ENOTDIR) + ERR; + return; + } + while (dirent = readdir(dir)) + { + /* build path */ + if (!pathncat(pathsub, sizeof(pathsub), path, dirent->d_name)) + { + ERR; + continue; + } + + /* check path */ + check_realpath_recurse(pathsub, depth - 1); + } + if (closedir(dir) < 0) ERR; +} + +#define PATH_DEPTH 3 +#define L(x) "/t43_link_" #x ".tmp" + +static char basepath[PATH_MAX + 1]; + +static char *addbasepath(char *buffer, const char *path) +{ + size_t basepathlen, pathlen; + int slashlen; + + /* assumption: both start with slash and neither end with it */ + assert(basepath[0] == '/'); + assert(basepath[strlen(basepath) - 1] != '/'); + assert(buffer); + assert(path); + assert(path[0] == '/'); + + /* check result length */ + basepathlen = strlen(basepath); + pathlen = strlen(path); + if (basepathlen + pathlen > PATH_MAX) + { + printf("path too long\n"); + exit(-1); + } + + /* concatenate base path and path */ + memcpy(buffer, basepath, basepathlen); + memcpy(buffer + basepathlen, path, pathlen + 1); + return buffer; +} + +static void cleanup(int silent) +{ + char buffer[PATH_MAX + 1]; + + if (unlink(addbasepath(buffer, L(1))) < 0 && !silent) ERR; + if (unlink(addbasepath(buffer, L(2))) < 0 && !silent) ERR; + if (unlink(addbasepath(buffer, L(3))) < 0 && !silent) ERR; + if (unlink(addbasepath(buffer, L(4))) < 0 && !silent) ERR; + if (unlink(addbasepath(buffer, L(5))) < 0 && !silent) ERR; +} + +int main(int argc, char **argv) +{ + char buffer1[PATH_MAX + 1], buffer2[PATH_MAX + 1]; + + /* initialize */ + printf("Test 43 "); + fflush(stdout); + executable = argv[0]; + getcwd(basepath, sizeof(basepath)); + cleanup(1); + + /* prepare some symlinks to make it more difficult */ + if (symlink("/", addbasepath(buffer1, L(1))) < 0) ERR; + if (symlink(basepath, addbasepath(buffer1, L(2))) < 0) ERR; + + /* perform some tests */ + check_realpath_recurse(basepath, PATH_DEPTH); + + /* now try with recursive symlinks */ + if (symlink(addbasepath(buffer1, L(3)), addbasepath(buffer2, L(3))) < 0) ERR; + if (symlink(addbasepath(buffer1, L(5)), addbasepath(buffer2, L(4))) < 0) ERR; + if (symlink(addbasepath(buffer1, L(4)), addbasepath(buffer2, L(5))) < 0) ERR; + check_realpath_step_by_step(addbasepath(buffer1, L(3)), ELOOP); + check_realpath_step_by_step(addbasepath(buffer1, L(4)), ELOOP); + check_realpath_step_by_step(addbasepath(buffer1, L(5)), ELOOP); + + /* delete the symlinks */ + cleanup(0); + + /* done */ + quit(); + return(-1); /* impossible */ +} -- 2.44.0