From c41b3a1a647155ab9c727dab168baf08e7609372 Mon Sep 17 00:00:00 2001 From: Matthias Melcher Date: Wed, 11 Oct 2023 22:26:36 +0100 Subject: [PATCH] Fixes fl_filename_relative on Linux, Mac, and Windows (#787) * fixed filename_relative for Linux * Fixing fl_filename_relative for MSWindows. * Update documentation * Fixed docs. * Fixes Linux and macOS builds --- FL/filename.H | 4 +- .../WinAPI/Fl_WinAPI_System_Driver.cxx | 163 +++++++++-------- src/filename_absolute.cxx | 173 +++++++++++------- 3 files changed, 196 insertions(+), 144 deletions(-) diff --git a/FL/filename.H b/FL/filename.H index be7e90ead..b5b76f92f 100644 --- a/FL/filename.H +++ b/FL/filename.H @@ -62,8 +62,10 @@ FL_EXPORT Fl_String fl_filename_path(const Fl_String &filename); FL_EXPORT Fl_String fl_filename_ext(const Fl_String &filename); FL_EXPORT Fl_String fl_filename_setext(const Fl_String &filename, const Fl_String &new_extension); FL_EXPORT Fl_String fl_filename_expand(const Fl_String &from); +FL_EXPORT int fl_filename_absolute(char *to, int tolen, const char *from, const char *cwd); FL_EXPORT Fl_String fl_filename_absolute(const Fl_String &from); FL_EXPORT Fl_String fl_filename_absolute(const Fl_String &from, const Fl_String &base); +FL_EXPORT int fl_filename_relative(char *to, int tolen, const char *from, const char *cwd); FL_EXPORT Fl_String fl_filename_relative(const Fl_String &from); FL_EXPORT Fl_String fl_filename_relative(const Fl_String &from, const Fl_String &base); FL_EXPORT Fl_String fl_getcwd(); @@ -77,9 +79,7 @@ FL_EXPORT Fl_String fl_getcwd(); inline char *fl_filename_setext(char *to, const char *ext) { return fl_filename_setext(to, FL_PATH_MAX, ext); } inline int fl_filename_expand(char *to, const char *from) { return fl_filename_expand(to, FL_PATH_MAX, from); } -FL_EXPORT int fl_filename_absolute(char *to, int tolen, const char *from, const char *cwd); inline int fl_filename_absolute(char *to, const char *from) { return fl_filename_absolute(to, FL_PATH_MAX, from); } -FL_EXPORT int fl_filename_relative(char *to, int tolen, const char *from, const char *cwd); inline int fl_filename_relative(char *to, const char *from) { return fl_filename_relative(to, FL_PATH_MAX, from); } # endif /* __cplusplus */ diff --git a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx index c2d41f98e..faf89e979 100644 --- a/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx +++ b/src/drivers/WinAPI/Fl_WinAPI_System_Driver.cxx @@ -549,90 +549,101 @@ int Fl_WinAPI_System_Driver::filename_expand(char *to, int tolen, const char *fr int // O - 0 if no change, 1 if changed Fl_WinAPI_System_Driver::filename_relative(char *to, // O - Relative filename - int tolen, // I - Size of "to" buffer - const char *from, // I - Absolute filename - const char *base) // I - Find path relative to this path + int tolen, // I - Size of "to" buffer + const char *dest_dir, // I - Absolute filename + const char *base_dir) // I - Find path relative to this path { - char *newslash; // Directory separator - const char *slash; // Directory separator - char *cwd = 0L, *cwd_buf = 0L; - if (base) cwd = cwd_buf = fl_strdup(base); + // Find the relative path from base_dir to dest_dir. + // Both paths must be absolute and well formed (contain no /../ and /./ segments). - // return if "from" is not an absolute path - if (from[0] == '\0' || - (!isdirsep(*from) && !isalpha(*from) && from[1] != ':' && - !isdirsep(from[2]))) { - strlcpy(to, from, tolen); - if (cwd_buf) free(cwd_buf); - return 0; - } - - // return if "cwd" is not an absolute path - if (!cwd || cwd[0] == '\0' || - (!isdirsep(*cwd) && !isalpha(*cwd) && cwd[1] != ':' && - !isdirsep(cwd[2]))) { - strlcpy(to, from, tolen); - if (cwd_buf) free(cwd_buf); - return 0; - } - - // convert all backslashes into forward slashes - for (newslash = strchr(cwd, '\\'); newslash; newslash = strchr(newslash + 1, '\\')) - *newslash = '/'; - - // test for the exact same string and return "." if so - if (!strcasecmp(from, cwd)) { - strlcpy(to, ".", tolen); - free(cwd_buf); - return (1); - } - - // test for the same drive. Return the absolute path if not - if (tolower(*from & 255) != tolower(*cwd & 255)) { - // Not the same drive... - strlcpy(to, from, tolen); - free(cwd_buf); + // return if any of the pointers is NULL + if (!to || !dest_dir || !base_dir) { return 0; } - // compare the path name without the drive prefix - from += 2; cwd += 2; - - // compare both path names until we find a difference - for (slash = from, newslash = cwd; - *slash != '\0' && *newslash != '\0'; - slash ++, newslash ++) - if (isdirsep(*slash) && isdirsep(*newslash)) continue; - else if (tolower(*slash & 255) != tolower(*newslash & 255)) break; - - // skip over trailing slashes - if ( *newslash == '\0' && *slash != '\0' && !isdirsep(*slash) - &&(newslash==cwd || !isdirsep(newslash[-1])) ) - newslash--; - - // now go back to the first character of the first differing paths segment - while (!isdirsep(*slash) && slash > from) slash --; - if (isdirsep(*slash)) slash ++; - - // do the same for the current dir - if (isdirsep(*newslash)) newslash --; - if (*newslash != '\0') - while (!isdirsep(*newslash) && newslash > cwd) newslash --; - - // prepare the destination buffer - to[0] = '\0'; - to[tolen - 1] = '\0'; - - // now add a "previous dir" sequence for every following slash in the cwd - while (*newslash != '\0') { - if (isdirsep(*newslash)) strlcat(to, "../", tolen); - newslash ++; + // if there is a drive letter, make sure both paths use the same drive + if ( base_dir[0] < 128 && isalpha(base_dir[0]) && base_dir[1] == ':' + && dest_dir[0] < 128 && isalpha(dest_dir[0]) && dest_dir[1] == ':') { + if (tolower(base_dir[0]) != tolower(dest_dir[0])) { + strlcpy(to, dest_dir, tolen); + return 0; + } + // same drive, so skip to the start of the path + base_dir += 2; + dest_dir += 2; } - // finally add the differing path from "from" - strlcat(to, slash, tolen); + // return if `base_dir` or `dest_dir` is not an absolute path + if (!isdirsep(*base_dir) || !isdirsep(*dest_dir)) { + strlcpy(to, dest_dir, tolen); + return 0; + } + + const char *base_i = base_dir; // iterator through the base directory string + const char *base_s = base_dir; // pointer to the last dir separator found + const char *dest_i = dest_dir; // iterator through the destination directory + const char *dest_s = dest_dir; // pointer to the last dir separator found + + // compare both path names until we find a difference + for (;;) { +#if 0 // case sensitive + base_i++; + dest_i++; + char b = *base_i, d = *dest_i; +#else // case insensitive + base_i += fl_utf8len1(*base_i); + int b = fl_tolower(fl_utf8decode(base_i, NULL, NULL)); + dest_i += fl_utf8len1(*dest_i); + int d = fl_tolower(fl_utf8decode(dest_i, NULL, NULL)); +#endif + int b0 = (b == 0) || (isdirsep(b)); + int d0 = (d == 0) || (isdirsep(d)); + if (b0 && d0) { + base_s = base_i; + dest_s = dest_i; + } + if (b == 0 || d == 0) + break; + if (b != d) + break; + } + // base_s and dest_s point at the last separator we found + // base_i and dest_i point at the first character that differs + + // test for the exact same string and return "." if so + if ( (base_i[0] == 0 || (isdirsep(base_i[0]) && base_i[1] == 0)) + && (dest_i[0] == 0 || (isdirsep(dest_i[0]) && dest_i[1] == 0))) { + strlcpy(to, ".", tolen); + return 0; + } + + // prepare the destination buffer + to[0] = '\0'; + to[tolen - 1] = '\0'; + + // count the directory segments remaining in `base_dir` + int n_up = 0; + for (;;) { + char b = *base_s++; + if (b == 0) + break; + if (isdirsep(b) && *base_s) + n_up++; + } + + // now add a "previous dir" sequence for every following slash in the cwd + if (n_up > 0) + strlcat(to, "..", tolen); + for (; n_up > 1; --n_up) + strlcat(to, "/..", tolen); + + // finally add the differing path from "from" + if (*dest_s) { + if (n_up) + strlcat(to, "/", tolen); + strlcat(to, dest_s + 1, tolen); + } - free(cwd_buf); return 1; } diff --git a/src/filename_absolute.cxx b/src/filename_absolute.cxx index 0d0155df1..46c44bfba 100644 --- a/src/filename_absolute.cxx +++ b/src/filename_absolute.cxx @@ -28,7 +28,7 @@ #include #include "flstring.h" -static inline int isdirsep(char c) {return c == '/';} +static inline int isdirsep(int c) {return c == '/';} /** Makes a filename absolute from a relative filename to the current working directory. \code @@ -124,22 +124,49 @@ int Fl_System_Driver::filename_absolute(char *to, int tolen, const char *from, c /** Makes a filename relative to the current working directory. - \code - #include - [..] - fl_chdir("/var/tmp/somedir"); // set cwd to /var/tmp/somedir - [..] - char out[FL_PATH_MAX]; - fl_filename_relative(out, sizeof(out), "/var/tmp/somedir/foo.txt"); // out="foo.txt", return=1 - fl_filename_relative(out, sizeof(out), "/var/tmp/foo.txt"); // out="../foo.txt", return=1 - fl_filename_relative(out, sizeof(out), "foo.txt"); // out="foo.txt", return=0 (no change) - fl_filename_relative(out, sizeof(out), "./foo.txt"); // out="./foo.txt", return=0 (no change) - fl_filename_relative(out, sizeof(out), "../foo.txt"); // out="../foo.txt", return=0 (no change) - \endcode - \param[out] to resulting relative filename - \param[in] tolen size of the relative filename buffer - \param[in] from absolute filename - \return 0 if no change, non zero otherwise + + Return the \a from path made relative to the working directory, similar to + C++17 `std::filesystem::path::lexically_relative`. This function can also be + called with a fourth argument for a user supplied \a base directory path + + These conversions are purely lexical. They do not check that the paths exist, + do not follow symlinks, and do not access the filesystem at all. + + Path arguments must be absolute (start at the root directory) and must not + contain `.` or `..` segments, or double separators. A single trailing + separator is ok. + + On Windows, path arguments must start with a drive name, e.g. `c:\`. + Windows network paths and other special paths starting + with a double separator are not supported (`\\cloud\drive\path`, + `\\?\`, etc.) . Separators can be `\` and `/` and will be preserved. + Newly created separators are alway the forward slash `/`. + + On Windows and macOS, the path segment tests are case insensitive. + + If the path can not be generated, \a from path is copied into the \a to + buffer and 0 is returned. + + \code + #include + [..] + fl_chdir("/var/tmp/somedir"); // set cwd to /var/tmp/somedir + [..] + char out[FL_PATH_MAX]; + fl_filename_relative(out, sizeof(out), "/var/tmp/somedir/foo.txt"); // out="foo.txt", return=1 + fl_filename_relative(out, sizeof(out), "/var/tmp/foo.txt"); // out="../foo.txt", return=1 + fl_filename_relative(out, sizeof(out), "foo.txt"); // out="foo.txt", return=0 (no change) + fl_filename_relative(out, sizeof(out), "./foo.txt"); // out="./foo.txt", return=0 (no change) + fl_filename_relative(out, sizeof(out), "../foo.txt"); // out="../foo.txt", return=0 (no change) + \endcode + + \param[out] to resulting relative filename + \param[in] tolen size of the relative filename buffer + \param[in] from absolute filename + \return 0 if no change, non zero otherwise + \see fl_filename_relative(char *to, int tolen, const char *from, const char *base) + \see fl_filename_relative(const Fl_String &from, const Fl_String &base) + \see fl_filename_relative(const Fl_String &from) */ int fl_filename_relative(char *to, int tolen, const char *from) { @@ -154,11 +181,13 @@ int fl_filename_relative(char *to, int tolen, const char *from) /** Makes a filename relative to any other directory. - \param[out] to resulting relative filename + + \param[out] to resulting relative filepath \param[in] tolen size of the relative filename buffer - \param[in] from absolute filename - \param[in] base generate filename relative to this absolute filename + \param[in] from absolute filepath + \param[in] base generate filepath relative to this absolute filepath \return 0 if no change, non zero otherwise + \see fl_filename_relative(char *to, int tolen, const char *from) */ int fl_filename_relative(char *to, int tolen, const char *from, const char *base) { return Fl::system_driver()->filename_relative(to, tolen, from, base); @@ -171,70 +200,82 @@ int fl_filename_relative(char *to, int tolen, const char *from, const char *base \{ */ -int Fl_System_Driver::filename_relative(char *to, int tolen, const char *from, const char *base) +int Fl_System_Driver::filename_relative(char *to, int tolen, const char *dest_dir, const char *base_dir) { - char *newslash; // Directory separator - const char *slash; // Directory separator - char *cwd = 0L, *cwd_buf = 0L; - if (base) cwd = cwd_buf = fl_strdup(base); + // Find the relative path from base_dir to dest_dir. + // Both paths must be absolute and well formed (contain no /../ and /./ segments). + const char *base_i = base_dir; // iterator through the base directory string + const char *base_s = base_dir; // pointer to the last dir separator found + const char *dest_i = dest_dir; // iterator through the destination directory + const char *dest_s = dest_dir; // pointer to the last dir separator found - // return if "from" is not an absolute path - if (from[0] == '\0' || !isdirsep(*from)) { - strlcpy(to, from, tolen); - if (cwd_buf) free(cwd_buf); + // return if any of the pointers is NULL + if (!to || !dest_dir || !base_dir) { return 0; } - // return if "cwd" is not an absolute path - if (!cwd || cwd[0] == '\0' || !isdirsep(*cwd)) { - strlcpy(to, from, tolen); - if (cwd_buf) free(cwd_buf); + // return if `base_dir` or `dest_dir` is not an absolute path + if (!isdirsep(*base_dir) || !isdirsep(*dest_dir)) { + strlcpy(to, dest_dir, tolen); return 0; } - // test for the exact same string and return "." if so - if (!strcmp(from, cwd)) { - strlcpy(to, ".", tolen); - free(cwd_buf); - return (1); - } - // compare both path names until we find a difference - for (slash = from, newslash = cwd; - *slash != '\0' && *newslash != '\0'; - slash ++, newslash ++) - if (isdirsep(*slash) && isdirsep(*newslash)) continue; - else if (*slash != *newslash) break; + for (;;) { +#ifndef __APPLE__ // case sensitive + base_i++; + dest_i++; + char b = *base_i, d = *dest_i; +#else // case insensitive + base_i += fl_utf8len1(*base_i); + int b = fl_tolower(fl_utf8decode(base_i, NULL, NULL)); + dest_i += fl_utf8len1(*dest_i); + int d = fl_tolower(fl_utf8decode(dest_i, NULL, NULL)); +#endif + int b0 = (b==0) || (isdirsep(b)); + int d0 = (d==0) || (isdirsep(d)); + if (b0 && d0) { + base_s = base_i; + dest_s = dest_i; + } + if (b==0 || d==0) break; + if (b!=d) break; + } + // base_s and dest_s point at the last separator we found + // base_i and dest_i point at the first character that differs - // skip over trailing slashes - if ( *newslash == '\0' && *slash != '\0' && !isdirsep(*slash) - &&(newslash==cwd || !isdirsep(newslash[-1])) ) - newslash--; - - // now go back to the first character of the first differing paths segment - while (!isdirsep(*slash) && slash > from) slash --; - if (isdirsep(*slash)) slash ++; - - // do the same for the current dir - if (isdirsep(*newslash)) newslash --; - if (*newslash != '\0') - while (!isdirsep(*newslash) && newslash > cwd) newslash --; + // test for the exact same string and return "." if so + if ( (base_i[0] == 0 || (isdirsep(base_i[0]) && base_i[1] == 0)) + && (dest_i[0] == 0 || (isdirsep(dest_i[0]) && dest_i[1] == 0))) { + strlcpy(to, ".", tolen); + return 0; + } // prepare the destination buffer to[0] = '\0'; to[tolen - 1] = '\0'; - // now add a "previous dir" sequence for every following slash in the cwd - while (*newslash != '\0') { - if (isdirsep(*newslash)) strlcat(to, "../", tolen); - - newslash ++; + // count the directory segments remaining in `base_dir` + int n_up = 0; + for (;;) { + char b = *base_s++; + if (b==0) break; + if (isdirsep(b) && *base_s) n_up++; } - // finally add the differing path from "from" - strlcat(to, slash, tolen); + // now add a "previous dir" sequence for every following slash in the cwd + if (n_up>0) + strlcat(to, "..", tolen); + for (; n_up>1; --n_up) + strlcat(to, "/..", tolen); + + // finally add the differing path from "from" + if (*dest_s) { + if (n_up) + strlcat(to, "/", tolen); + strlcat(to, dest_s+1, tolen); + } - free(cwd_buf); return 1; }