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:
Tyler Miller
2023-08-01 08:28:28 -07:00
committed by GitHub
parent dfe19d6e00
commit 0804034c07
4 changed files with 99 additions and 56 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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
---@param hex string
---@return string
local function hex_to_char(hex) local function hex_to_char(hex)
return schar(tonumber(hex, 16)) return schar(tonumber(hex, 16))
end end
uri_decode = function(str)
return str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', hex_to_char)
end
end
local uri_encode
do
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%-._~!$&'()*+,;=:@/",
}
local sbyte = string.byte
local tohex = require('bit').tohex
---@param char string
---@return string
local function percent_encode_char(char) local function percent_encode_char(char)
return '%' .. tohex(sbyte(char), 2) return '%' .. tohex(sbyte(char), 2)
end 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
---@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,7 +104,7 @@ 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:/+', '')
@@ -112,7 +115,7 @@ 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

View File

@@ -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)