mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	fix(loader): cache path ambiguity #24491
Problem: cache paths are derived by replacing each reserved/filesystem- path-sensitive char with a `%` char in the original path. With this method, two different files at two different paths (each containing `%` chars) can erroneously resolve to the very same cache path in certain edge-cases. Solution: derive cache paths by url-encoding the original (path) instead using `vim.uri_encode()` with `"rfc2396"`. Increment `Loader.VERSION` to denote this change.
This commit is contained in:
		| @@ -2428,8 +2428,27 @@ vim.loader.reset({path})                                  *vim.loader.reset()* | |||||||
| ============================================================================== | ============================================================================== | ||||||
| Lua module: vim.uri                                                  *vim.uri* | Lua module: vim.uri                                                  *vim.uri* | ||||||
|  |  | ||||||
|  | vim.uri_decode({str})                                       *vim.uri_decode()* | ||||||
|  |     URI-decodes a string containing percent escapes. | ||||||
|  |  | ||||||
|  |     Parameters: ~ | ||||||
|  |       • {str}  (string) string to decode | ||||||
|  |  | ||||||
|  |     Return: ~ | ||||||
|  |         (string) decoded string | ||||||
|  |  | ||||||
|  | vim.uri_encode({str}, {rfc})                                *vim.uri_encode()* | ||||||
|  |     URI-encodes a string using percent escapes. | ||||||
|  |  | ||||||
|  |     Parameters: ~ | ||||||
|  |       • {str}  (string) string to encode | ||||||
|  |       • {rfc}  "rfc2396" | "rfc2732" | "rfc3986" | nil | ||||||
|  |  | ||||||
|  |     Return: ~ | ||||||
|  |         (string) encoded string | ||||||
|  |  | ||||||
| vim.uri_from_bufnr({bufnr})                             *vim.uri_from_bufnr()* | vim.uri_from_bufnr({bufnr})                             *vim.uri_from_bufnr()* | ||||||
|     Get a URI from a bufnr |     Gets a URI from a bufnr. | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {bufnr}  (integer) |       • {bufnr}  (integer) | ||||||
| @@ -2438,7 +2457,7 @@ vim.uri_from_bufnr({bufnr})                             *vim.uri_from_bufnr()* | |||||||
|         (string) URI |         (string) URI | ||||||
|  |  | ||||||
| vim.uri_from_fname({path})                              *vim.uri_from_fname()* | vim.uri_from_fname({path})                              *vim.uri_from_fname()* | ||||||
|     Get a URI from a file path. |     Gets a URI from a file path. | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {path}  (string) Path to file |       • {path}  (string) Path to file | ||||||
| @@ -2447,7 +2466,7 @@ vim.uri_from_fname({path})                              *vim.uri_from_fname()* | |||||||
|         (string) URI |         (string) URI | ||||||
|  |  | ||||||
| vim.uri_to_bufnr({uri})                                   *vim.uri_to_bufnr()* | vim.uri_to_bufnr({uri})                                   *vim.uri_to_bufnr()* | ||||||
|     Get the buffer for a uri. Creates a new unloaded buffer if no buffer for |     Gets the buffer for a uri. Creates a new unloaded buffer if no buffer for | ||||||
|     the uri already exists. |     the uri already exists. | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
| @@ -2457,7 +2476,7 @@ vim.uri_to_bufnr({uri})                                   *vim.uri_to_bufnr()* | |||||||
|         (integer) bufnr |         (integer) bufnr | ||||||
|  |  | ||||||
| vim.uri_to_fname({uri})                                   *vim.uri_to_fname()* | vim.uri_to_fname({uri})                                   *vim.uri_to_fname()* | ||||||
|     Get a filename from a URI |     Gets a filename from a URI. | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {uri}  (string) |       • {uri}  (string) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| local uv = vim.uv | local uv = vim.uv | ||||||
|  | local uri_encode = vim.uri_encode | ||||||
|  |  | ||||||
| --- @type (fun(modename: string): fun()|string)[] | --- @type (fun(modename: string): fun()|string)[] | ||||||
| local loaders = package.loaders | local loaders = package.loaders | ||||||
| @@ -33,7 +34,7 @@ M.enabled = false | |||||||
| ---@field _rtp_key string | ---@field _rtp_key string | ||||||
| ---@field _hashes? table<string, CacheHash> | ---@field _hashes? table<string, CacheHash> | ||||||
| local Loader = { | local Loader = { | ||||||
|   VERSION = 3, |   VERSION = 4, | ||||||
|   ---@type table<string, table<string,ModuleInfo>> |   ---@type table<string, table<string,ModuleInfo>> | ||||||
|   _indexed = {}, |   _indexed = {}, | ||||||
|   ---@type table<string, string[]> |   ---@type table<string, string[]> | ||||||
| @@ -99,7 +100,7 @@ end | |||||||
| ---@return string file_name | ---@return string file_name | ||||||
| ---@private | ---@private | ||||||
| function Loader.cache_file(name) | function Loader.cache_file(name) | ||||||
|   local ret = M.path .. '/' .. name:gsub('[/\\:]', '%%') |   local ret = ('%s/%s'):format(M.path, uri_encode(name, 'rfc2396')) | ||||||
|   return ret:sub(-4) == '.lua' and (ret .. 'c') or (ret .. '.luac') |   return ret:sub(-4) == '.lua' and (ret .. 'c') or (ret .. '.luac') | ||||||
| end | end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,65 +1,71 @@ | |||||||
| --- TODO: This is implemented only for files now. | ---TODO: This is implemented only for files currently. | ||||||
| -- https://tools.ietf.org/html/rfc3986 | -- https://tools.ietf.org/html/rfc3986 | ||||||
| -- https://tools.ietf.org/html/rfc2732 | -- https://tools.ietf.org/html/rfc2732 | ||||||
| -- https://tools.ietf.org/html/rfc2396 | -- https://tools.ietf.org/html/rfc2396 | ||||||
|  |  | ||||||
| local uri_decode | local M = {} | ||||||
| do | local sbyte = string.byte | ||||||
|   local schar = string.char | local schar = string.char | ||||||
|  | local tohex = require('bit').tohex | ||||||
|  | local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*' | ||||||
|  | local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*' | ||||||
|  | local PATTERNS = { | ||||||
|  |   ---RFC 2396 | ||||||
|  |   ---https://tools.ietf.org/html/rfc2396#section-2.2 | ||||||
|  |   rfc2396 = "^A-Za-z0-9%-_.!~*'()", | ||||||
|  |   ---RFC 2732 | ||||||
|  |   ---https://tools.ietf.org/html/rfc2732 | ||||||
|  |   rfc2732 = "^A-Za-z0-9%-_.!~*'()[]", | ||||||
|  |   ---RFC 3986 | ||||||
|  |   ---https://tools.ietf.org/html/rfc3986#section-2.2 | ||||||
|  |   rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/", | ||||||
|  | } | ||||||
|  |  | ||||||
|   --- Convert hex to char | ---Converts hex to char | ||||||
|   local function hex_to_char(hex) | ---@param hex string | ||||||
|     return schar(tonumber(hex, 16)) | ---@return string | ||||||
|   end | local function hex_to_char(hex) | ||||||
|   uri_decode = function(str) |   return schar(tonumber(hex, 16)) | ||||||
|     return str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char) |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
| local uri_encode | ---@param char string | ||||||
| do | ---@return string | ||||||
|   local PATTERNS = { | local function percent_encode_char(char) | ||||||
|     --- RFC 2396 |   return '%' .. tohex(sbyte(char), 2) | ||||||
|     -- https://tools.ietf.org/html/rfc2396#section-2.2 |  | ||||||
|     rfc2396 = "^A-Za-z0-9%-_.!~*'()", |  | ||||||
|     --- RFC 2732 |  | ||||||
|     -- https://tools.ietf.org/html/rfc2732 |  | ||||||
|     rfc2732 = "^A-Za-z0-9%-_.!~*'()[]", |  | ||||||
|     --- RFC 3986 |  | ||||||
|     -- https://tools.ietf.org/html/rfc3986#section-2.2 |  | ||||||
|     rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/", |  | ||||||
|   } |  | ||||||
|   local sbyte = string.byte |  | ||||||
|   local tohex = require('bit').tohex |  | ||||||
|  |  | ||||||
|   local function percent_encode_char(char) |  | ||||||
|     return '%' .. tohex(sbyte(char), 2) |  | ||||||
|   end |  | ||||||
|   uri_encode = function(text, rfc) |  | ||||||
|     if not text then |  | ||||||
|       return |  | ||||||
|     end |  | ||||||
|     local pattern = PATTERNS[rfc] or PATTERNS.rfc3986 |  | ||||||
|     return text:gsub('([' .. pattern .. '])', percent_encode_char) |  | ||||||
|   end |  | ||||||
| end | end | ||||||
|  |  | ||||||
|  | ---@param uri string | ||||||
|  | ---@return boolean | ||||||
| local function is_windows_file_uri(uri) | local function is_windows_file_uri(uri) | ||||||
|   return uri:match('^file:/+[a-zA-Z]:') ~= nil |   return uri:match('^file:/+[a-zA-Z]:') ~= nil | ||||||
| end | end | ||||||
|  |  | ||||||
| local M = {} | ---URI-encodes a string using percent escapes. | ||||||
|  | ---@param str string string to encode | ||||||
|  | ---@param rfc "rfc2396" | "rfc2732" | "rfc3986" | nil | ||||||
|  | ---@return string encoded string | ||||||
|  | function M.uri_encode(str, rfc) | ||||||
|  |   local pattern = PATTERNS[rfc] or PATTERNS.rfc3986 | ||||||
|  |   return (str:gsub('([' .. pattern .. '])', percent_encode_char)) -- clamped to 1 retval with () | ||||||
|  | end | ||||||
|  |  | ||||||
| --- Get a URI from a file path. | ---URI-decodes a string containing percent escapes. | ||||||
|  | ---@param str string string to decode | ||||||
|  | ---@return string decoded string | ||||||
|  | function M.uri_decode(str) | ||||||
|  |   return (str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)) -- clamped to 1 retval with () | ||||||
|  | end | ||||||
|  |  | ||||||
|  | ---Gets a URI from a file path. | ||||||
| ---@param path string Path to file | ---@param path string Path to file | ||||||
| ---@return string URI | ---@return string URI | ||||||
| function M.uri_from_fname(path) | function M.uri_from_fname(path) | ||||||
|   local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') |   local volume_path, fname = path:match('^([a-zA-Z]:)(.*)') | ||||||
|   local is_windows = volume_path ~= nil |   local is_windows = volume_path ~= nil | ||||||
|   if is_windows then |   if is_windows then | ||||||
|     path = volume_path .. uri_encode(fname:gsub('\\', '/')) |     path = volume_path .. M.uri_encode(fname:gsub('\\', '/')) | ||||||
|   else |   else | ||||||
|     path = uri_encode(path) |     path = M.uri_encode(path) | ||||||
|   end |   end | ||||||
|   local uri_parts = { 'file://' } |   local uri_parts = { 'file://' } | ||||||
|   if is_windows then |   if is_windows then | ||||||
| @@ -69,10 +75,7 @@ function M.uri_from_fname(path) | |||||||
|   return table.concat(uri_parts) |   return table.concat(uri_parts) | ||||||
| end | end | ||||||
|  |  | ||||||
| local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):.*' | ---Gets a URI from a bufnr. | ||||||
| local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9.+-]*):[a-zA-Z]:.*' |  | ||||||
|  |  | ||||||
| --- Get a URI from a bufnr |  | ||||||
| ---@param bufnr integer | ---@param bufnr integer | ||||||
| ---@return string URI | ---@return string URI | ||||||
| function M.uri_from_bufnr(bufnr) | function M.uri_from_bufnr(bufnr) | ||||||
| @@ -93,7 +96,7 @@ function M.uri_from_bufnr(bufnr) | |||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Get a filename from a URI | ---Gets a filename from a URI. | ||||||
| ---@param uri string | ---@param uri string | ||||||
| ---@return string filename or unchanged URI for non-file URIs | ---@return string filename or unchanged URI for non-file URIs | ||||||
| function M.uri_to_fname(uri) | function M.uri_to_fname(uri) | ||||||
| @@ -101,8 +104,8 @@ function M.uri_to_fname(uri) | |||||||
|   if scheme ~= 'file' then |   if scheme ~= 'file' then | ||||||
|     return uri |     return uri | ||||||
|   end |   end | ||||||
|   uri = uri_decode(uri) |   uri = M.uri_decode(uri) | ||||||
|   -- TODO improve this. |   --TODO improve this. | ||||||
|   if is_windows_file_uri(uri) then |   if is_windows_file_uri(uri) then | ||||||
|     uri = uri:gsub('^file:/+', '') |     uri = uri:gsub('^file:/+', '') | ||||||
|     uri = uri:gsub('/', '\\') |     uri = uri:gsub('/', '\\') | ||||||
| @@ -112,8 +115,8 @@ function M.uri_to_fname(uri) | |||||||
|   return uri |   return uri | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Get the buffer for a uri. | ---Gets the buffer for a uri. | ||||||
| --- Creates a new unloaded buffer if no buffer for the uri already exists. | ---Creates a new unloaded buffer if no buffer for the uri already exists. | ||||||
| -- | -- | ||||||
| ---@param uri string | ---@param uri string | ||||||
| ---@return integer bufnr | ---@return integer bufnr | ||||||
|   | |||||||
| @@ -33,4 +33,24 @@ describe('vim.loader', function() | |||||||
|       return _G.TEST |       return _G.TEST | ||||||
|     ]], tmp)) |     ]], tmp)) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|  |   it('handles % signs in modpath (#24491)', function() | ||||||
|  |     exec_lua[[ | ||||||
|  |       vim.loader.enable() | ||||||
|  |     ]] | ||||||
|  |  | ||||||
|  |     local tmp1, tmp2 = (function (t) | ||||||
|  |       assert(os.remove(t)) | ||||||
|  |       assert(helpers.mkdir(t)) | ||||||
|  |       assert(helpers.mkdir(t .. '/%')) | ||||||
|  |       return t .. '/%/x', t .. '/%%x' | ||||||
|  |     end)(helpers.tmpname()) | ||||||
|  |  | ||||||
|  |     helpers.write_file(tmp1, 'return 1', true) | ||||||
|  |     helpers.write_file(tmp2, 'return 2', true) | ||||||
|  |     vim.uv.fs_utime(tmp1, 0, 0) | ||||||
|  |     vim.uv.fs_utime(tmp2, 0, 0) | ||||||
|  |     eq(1, exec_lua('return loadfile(...)()', tmp1)) | ||||||
|  |     eq(2, exec_lua('return loadfile(...)()', tmp2)) | ||||||
|  |   end) | ||||||
| end) | end) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tyler Miller
					Tyler Miller