diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index 796d66f74c..7b9d97e4ee 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -1396,7 +1396,10 @@ char *find_file_in_path_option(char *ptr, size_t len, int options, int first, ch // filename on the first call. if (first == true) { if (path_with_url(*file_to_find)) { - file_name = xstrdup(*file_to_find); + file_name = + strncmp(*file_to_find, "file:/", 6) == 0 ? + xstrdup(handle_file_path_prefix(*file_to_find)) : + xstrdup(*file_to_find); goto theend; } diff --git a/src/nvim/path.c b/src/nvim/path.c index 15a8762da1..d3064f5e31 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -2467,3 +2467,73 @@ void path_guess_exepath(const char *argv0, char *buf, size_t bufsize) xstrlcpy(buf, argv0, bufsize); } } + +/// Handles file path that starts with `file:[/|///]` +/// +/// One and three slashes mean that file location is localhost/local. +/// Example: +/// - file:/path/to/file refers to /path/to/file +/// - file:///path/to/file refers to /path/to/file (host is omitted) +/// But, with two slashes, it refers to a file location on a host machine. +/// - file://host/path/to/file +/// Example: +/// - file://localhost/path/to/file refers to /path/to/file locally +/// When encountering two slashes, don't alter file_path. +/// +/// Reference: +/// https://en.wikipedia.org/wiki/File_URI_scheme#Number_of_slash_characters +/// +/// @param[in] file_path Full name of file path starts with "file:/" +/// +/// @return either truncated version of file path or its input. +char *handle_file_path_prefix(char *file_path) +{ + const char *reader = file_path + 5; // file: <- start there + int slash_count = 0; + while (reader[0] == '/') { + slash_count += 1; + reader += 1; + } + if (slash_count == 1 || slash_count == 3) { +#ifdef MSWIN + return decode_window_filepath(file_path, slash_count); +#else + return strchr(file_path, '/'); +#endif + } + return file_path; +} + +/// Decode hexcode that could be part of file path. +/// +/// Convert hex code, such as "%3A" into ":" +/// Example: 'file:///C%3A/My%20Docuemnts' -> 'file:///C:/My Documents' +/// +/// @param[in] file_path Full name of file path starts with "file:/" +/// @param[in] slash_count count of slashes after "file" +/// +/// @return either truncated version of file path or its input. +char *decode_window_filepath(char *file_path, int slash_count) +{ + char *buf = xmalloc(MAXPATHL); + char *starting_point = file_path + 5 + slash_count; + int buf_counter = 0; + for (char *ptr = starting_point; ptr != NULL; ptr++) { + if (ptr[0] == '%' + && (('0' <= ptr[1] && ptr[1] <= '9') || ('A' <= ptr[1] && ptr[1] <= 'F')) + && (('0' <= ptr[2] && ptr[2] <= '9') || ('A' <= ptr[2] && ptr[2] <= 'F'))) { + // Deal with hex code + char hex_code[2] = { ptr[1], ptr[2] }; + buf[buf_counter] = (char)strtol(hex_code, NULL, 16); + ptr += 3; + } else { + // Copy the character + buf[buf_counter] = ptr[0]; + } + buf_counter += 1; + } + + xstrlcpy(file_path, buf, (unsigned long)buf_counter); // truncate + + return file_path; +} diff --git a/test/functional/core/path_spec.lua b/test/functional/core/path_spec.lua index 97c32f7de6..3aee2b1e96 100644 --- a/test/functional/core/path_spec.lua +++ b/test/functional/core/path_spec.lua @@ -13,7 +13,7 @@ local write_file = helpers.write_file local function join_path(...) local pathsep = (is_os('win') and '\\' or '/') - return table.concat({...}, pathsep) + return table.concat({ ... }, pathsep) end describe('path collapse', function() @@ -103,6 +103,31 @@ describe('file search', function() eq('filename_with_unicode_ααα', eval('expand("%:t")')) end) + it('finds "file:[/|///]" URI on the local filesystem #24032', function() + local iswin = is_os('win') + local function test_gf(input, expected_default, expected_win) + local expected = (iswin and expected_win or expected_default) + command('%delete') + insert(input) + command('norm! 0') + feed('gf') + eq(expected, eval('expand("%:p")')) + end + + test_gf("file:/a", "/a") + test_gf("file:/foo/bar/slash.txt", "/foo/bar/slash.txt") + test_gf("file:///foo/bar/3slashes.txt", "///foo/bar/3slashes.txt") + test_gf("file:/c:/test/window/slash.txt", "/c:/test/window/slash.txt") + test_gf("file:///c:/test/window/3slash.txt", "///c:/test/window/3slash.txt") + -- Test out window cases. + test_gf("file:///c%3A/test%20with%20%25/path", "///c%3A/test%20with%20%25/path", [[c:\test with %\path]]) + test_gf("file:///c%3A/documents%20and%24zero%/", "///c%3A/documents%20and%24zero%/", [[c:/documents and$zero$]]) + + test_gf("http://foo/bar/file:/with/url", "http://foo/bar/file:/with/url") + test_gf("file://host/foo/bar/2slashes.txt", "file://host/foo/bar/2slashes.txt") + test_gf("file://host/c:/2slashes.txt", "file://host/c:/2slashes.txt") + end) + it('gf/ matches Windows drive-letter filepaths (without ":" in &isfname)', function() local iswin = is_os('win') local function test_cfile(input, expected, expected_win)