diff options
Diffstat (limited to 'src/path.c')
-rw-r--r-- | src/path.c | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/src/path.c b/src/path.c index d1b3dcc05c..517d0fa42a 100644 --- a/src/path.c +++ b/src/path.c @@ -1301,3 +1301,172 @@ addfile ( ((char_u **)gap->ga_data)[gap->ga_len++] = p; } #endif /* !NO_EXPANDPATH */ + +/* + * Converts a file name into a canonical form. It simplifies a file name into + * its simplest form by stripping out unneeded components, if any. The + * resulting file name is simplified in place and will either be the same + * length as that supplied, or shorter. + */ +void simplify_filename(char_u *filename) +{ + int components = 0; + char_u *p, *tail, *start; + int stripping_disabled = FALSE; + int relative = TRUE; + + p = filename; +#ifdef BACKSLASH_IN_FILENAME + if (p[1] == ':') /* skip "x:" */ + p += 2; +#endif + + if (vim_ispathsep(*p)) { + relative = FALSE; + do + ++p; + while (vim_ispathsep(*p)); + } + start = p; /* remember start after "c:/" or "/" or "///" */ + + do { + /* At this point "p" is pointing to the char following a single "/" + * or "p" is at the "start" of the (absolute or relative) path name. */ + if (vim_ispathsep(*p)) + STRMOVE(p, p + 1); /* remove duplicate "/" */ + else if (p[0] == '.' && (vim_ispathsep(p[1]) || p[1] == NUL)) { + if (p == start && relative) + p += 1 + (p[1] != NUL); /* keep single "." or leading "./" */ + else { + /* Strip "./" or ".///". If we are at the end of the file name + * and there is no trailing path separator, either strip "/." if + * we are after "start", or strip "." if we are at the beginning + * of an absolute path name . */ + tail = p + 1; + if (p[1] != NUL) + while (vim_ispathsep(*tail)) + mb_ptr_adv(tail); + else if (p > start) + --p; /* strip preceding path separator */ + STRMOVE(p, tail); + } + } else if (p[0] == '.' && p[1] == '.' && + (vim_ispathsep(p[2]) || p[2] == NUL)) { + /* Skip to after ".." or "../" or "..///". */ + tail = p + 2; + while (vim_ispathsep(*tail)) + mb_ptr_adv(tail); + + if (components > 0) { /* strip one preceding component */ + int do_strip = FALSE; + char_u saved_char; + struct stat st; + + /* Don't strip for an erroneous file name. */ + if (!stripping_disabled) { + /* If the preceding component does not exist in the file + * system, we strip it. On Unix, we don't accept a symbolic + * link that refers to a non-existent file. */ + saved_char = p[-1]; + p[-1] = NUL; +#ifdef UNIX + if (mch_lstat((char *)filename, &st) < 0) +#else + if (mch_stat((char *)filename, &st) < 0) +#endif + do_strip = TRUE; + p[-1] = saved_char; + + --p; + /* Skip back to after previous '/'. */ + while (p > start && !after_pathsep(start, p)) + mb_ptr_back(start, p); + + if (!do_strip) { + /* If the component exists in the file system, check + * that stripping it won't change the meaning of the + * file name. First get information about the + * unstripped file name. This may fail if the component + * to strip is not a searchable directory (but a regular + * file, for instance), since the trailing "/.." cannot + * be applied then. We don't strip it then since we + * don't want to replace an erroneous file name by + * a valid one, and we disable stripping of later + * components. */ + saved_char = *tail; + *tail = NUL; + if (mch_stat((char *)filename, &st) >= 0) + do_strip = TRUE; + else + stripping_disabled = TRUE; + *tail = saved_char; +#ifdef UNIX + if (do_strip) { + struct stat new_st; + + /* On Unix, the check for the unstripped file name + * above works also for a symbolic link pointing to + * a searchable directory. But then the parent of + * the directory pointed to by the link must be the + * same as the stripped file name. (The latter + * exists in the file system since it is the + * component's parent directory.) */ + if (p == start && relative) + (void)mch_stat(".", &new_st); + else { + saved_char = *p; + *p = NUL; + (void)mch_stat((char *)filename, &new_st); + *p = saved_char; + } + + if (new_st.st_ino != st.st_ino || + new_st.st_dev != st.st_dev) { + do_strip = FALSE; + /* We don't disable stripping of later + * components since the unstripped path name is + * still valid. */ + } + } +#endif + } + } + + if (!do_strip) { + /* Skip the ".." or "../" and reset the counter for the + * components that might be stripped later on. */ + p = tail; + components = 0; + } else { + /* Strip previous component. If the result would get empty + * and there is no trailing path separator, leave a single + * "." instead. If we are at the end of the file name and + * there is no trailing path separator and a preceding + * component is left after stripping, strip its trailing + * path separator as well. */ + if (p == start && relative && tail[-1] == '.') { + *p++ = '.'; + *p = NUL; + } else { + if (p > start && tail[-1] == '.') + --p; + STRMOVE(p, tail); /* strip previous component */ + } + + --components; + } + } else if (p == start && !relative) /* leading "/.." or "/../" */ + STRMOVE(p, tail); /* strip ".." or "../" */ + else { + if (p == start + 2 && p[-2] == '.') { /* leading "./../" */ + STRMOVE(p - 2, p); /* strip leading "./" */ + tail -= 2; + } + p = tail; /* skip to char after ".." or "../" */ + } + } else { + ++components; /* simple path component */ + p = getnextcomp(p); + } + } while (*p != NUL); +} |