mirror of
https://github.com/neovim/neovim
synced 2025-07-16 01:01:49 +00:00
fix(trust): support for trusting directories #33735
Problem:
Directories that are "trusted" by `vim.secure.read()`, are not detectable later
(they will prompt again). https://github.com/neovim/neovim/discussions/33587#discussioncomment-12925887
Solution:
`vim.secure.read()` returns `true` if the user trusts a directory.
Also fix other bugs:
- If `f:read('*a')` returns `nil`, we treat that as a successful read of
the file, and hash it. `f:read` returns `nil` for directories, but
it's also documented as returning `nil` "if it cannot read data with the
specified format". I reworked the implementation so we explicitly
treat directories differently. Rather than hashing `nil` to put in the
trust database, we now put "directory" in there explicitly*.
- `vim.secure.trust` (used by `:trust`) didn't actually work for
directories, as it would blindly read the contents of a netrw buffer
and hash it. Now it uses the same codepath as `vim.secure.read`, and
as a result, works correctly for directories.
(cherry picked from commit 272dba7f07
)
This commit is contained in:
committed by
GitHub
parent
4b6caa913c
commit
560c6ca947
@ -3740,19 +3740,25 @@ vim.regex({re}) *vim.regex()*
|
||||
Lua module: vim.secure *vim.secure*
|
||||
|
||||
vim.secure.read({path}) *vim.secure.read()*
|
||||
Attempt to read the file at {path} prompting the user if the file should
|
||||
be trusted. The user's choice is persisted in a trust database at
|
||||
If {path} is a file: attempt to read the file, prompting the user if the
|
||||
file should be trusted.
|
||||
|
||||
If {path} is a directory: return true if the directory is trusted
|
||||
(non-recursive), prompting the user as necessary.
|
||||
|
||||
The user's choice is persisted in a trust database at
|
||||
$XDG_STATE_HOME/nvim/trust.
|
||||
|
||||
Attributes: ~
|
||||
Since: 0.9.0
|
||||
|
||||
Parameters: ~
|
||||
• {path} (`string`) Path to a file to read.
|
||||
• {path} (`string`) Path to a file or directory to read.
|
||||
|
||||
Return: ~
|
||||
(`string?`) The contents of the given file if it exists and is
|
||||
trusted, or nil otherwise.
|
||||
(`boolean|string?`) If {path} is not trusted or does not exist,
|
||||
returns `nil`. Otherwise, returns the contents of {path} if it is a
|
||||
file, or true if {path} is a directory.
|
||||
|
||||
See also: ~
|
||||
• |:trust|
|
||||
|
@ -171,6 +171,9 @@ API
|
||||
aligned text that truncates before covering up buffer text.
|
||||
• `virt_lines_overflow` field accepts value `scroll` to enable horizontal
|
||||
scrolling for virtual lines with 'nowrap'.
|
||||
• |vim.secure.read()| now returns `true` for trusted directories. Previously
|
||||
it would return `nil`, which made it impossible to tell if the directory was
|
||||
actually trusted.
|
||||
|
||||
DEFAULTS
|
||||
|
||||
|
@ -21,6 +21,50 @@ local function read_trust()
|
||||
return trust
|
||||
end
|
||||
|
||||
--- If {fullpath} is a file, read the contents of {fullpath} (or the contents of {bufnr}
|
||||
--- if given) and returns the contents and a hash of the contents.
|
||||
---
|
||||
--- If {fullpath} is a directory, then nothing is read from the filesystem, and
|
||||
--- `contents = true` and `hash = "directory"` is returned instead.
|
||||
---
|
||||
---@param fullpath (string) Path to a file or directory to read.
|
||||
---@param bufnr (number?) The number of the buffer.
|
||||
---@return string|boolean? contents the contents of the file, or true if it's a directory
|
||||
---@return string? hash the hash of the contents, or "directory" if it's a directory
|
||||
local function compute_hash(fullpath, bufnr)
|
||||
local contents ---@type string|boolean?
|
||||
local hash ---@type string
|
||||
if vim.fn.isdirectory(fullpath) == 1 then
|
||||
return true, 'directory'
|
||||
end
|
||||
|
||||
if bufnr then
|
||||
local newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
|
||||
contents =
|
||||
table.concat(vim.api.nvim_buf_get_lines(bufnr --[[@as integer]], 0, -1, false), newline)
|
||||
if vim.bo[bufnr].endofline then
|
||||
contents = contents .. newline
|
||||
end
|
||||
else
|
||||
do
|
||||
local f = io.open(fullpath, 'r')
|
||||
if not f then
|
||||
return nil, nil
|
||||
end
|
||||
contents = f:read('*a')
|
||||
f:close()
|
||||
end
|
||||
|
||||
if not contents then
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
hash = vim.fn.sha256(contents)
|
||||
|
||||
return contents, hash
|
||||
end
|
||||
|
||||
--- Writes provided {trust} table to trust database at
|
||||
--- $XDG_STATE_HOME/nvim/trust.
|
||||
---
|
||||
@ -37,17 +81,22 @@ local function write_trust(trust)
|
||||
f:close()
|
||||
end
|
||||
|
||||
--- Attempt to read the file at {path} prompting the user if the file should be
|
||||
--- trusted. The user's choice is persisted in a trust database at
|
||||
--- If {path} is a file: attempt to read the file, prompting the user if the file should be
|
||||
--- trusted.
|
||||
---
|
||||
--- If {path} is a directory: return true if the directory is trusted (non-recursive), prompting
|
||||
--- the user as necessary.
|
||||
---
|
||||
--- The user's choice is persisted in a trust database at
|
||||
--- $XDG_STATE_HOME/nvim/trust.
|
||||
---
|
||||
---@since 11
|
||||
---@see |:trust|
|
||||
---
|
||||
---@param path (string) Path to a file to read.
|
||||
---@param path (string) Path to a file or directory to read.
|
||||
---
|
||||
---@return (string|nil) The contents of the given file if it exists and is
|
||||
--- trusted, or nil otherwise.
|
||||
---@return (boolean|string|nil) If {path} is not trusted or does not exist, returns `nil`. Otherwise,
|
||||
--- returns the contents of {path} if it is a file, or true if {path} is a directory.
|
||||
function M.read(path)
|
||||
vim.validate('path', path, 'string')
|
||||
local fullpath = vim.uv.fs_realpath(vim.fs.normalize(path))
|
||||
@ -62,26 +111,25 @@ function M.read(path)
|
||||
return nil
|
||||
end
|
||||
|
||||
local contents ---@type string?
|
||||
do
|
||||
local f = io.open(fullpath, 'r')
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
contents = f:read('*a')
|
||||
f:close()
|
||||
local contents, hash = compute_hash(fullpath, nil)
|
||||
if not contents then
|
||||
return nil
|
||||
end
|
||||
|
||||
local hash = vim.fn.sha256(contents)
|
||||
if trust[fullpath] == hash then
|
||||
-- File already exists in trust database
|
||||
return contents
|
||||
end
|
||||
|
||||
local dir_msg = ''
|
||||
if hash == 'directory' then
|
||||
dir_msg = ' DIRECTORY trust is decided only by its name, not its contents.'
|
||||
end
|
||||
|
||||
-- File either does not exist in trust database or the hash does not match
|
||||
local ok, result = pcall(
|
||||
vim.fn.confirm,
|
||||
string.format('%s is not trusted.', fullpath),
|
||||
string.format('%s is not trusted.%s', fullpath, dir_msg),
|
||||
'&ignore\n&view\n&deny\n&allow',
|
||||
1
|
||||
)
|
||||
@ -169,13 +217,10 @@ function M.trust(opts)
|
||||
local trust = read_trust()
|
||||
|
||||
if action == 'allow' then
|
||||
local newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
|
||||
local contents =
|
||||
table.concat(vim.api.nvim_buf_get_lines(bufnr --[[@as integer]], 0, -1, false), newline)
|
||||
if vim.bo[bufnr].endofline then
|
||||
contents = contents .. newline
|
||||
local contents, hash = compute_hash(fullpath, bufnr)
|
||||
if not contents then
|
||||
return false, string.format('could not read path: %s', fullpath)
|
||||
end
|
||||
local hash = vim.fn.sha256(contents)
|
||||
|
||||
trust[fullpath] = hash
|
||||
elseif action == 'deny' then
|
||||
|
@ -20,25 +20,33 @@ local read_file = t.read_file
|
||||
describe('vim.secure', function()
|
||||
describe('read()', function()
|
||||
local xstate = 'Xstate'
|
||||
local screen ---@type test.functional.ui.screen
|
||||
|
||||
setup(function()
|
||||
before_each(function()
|
||||
clear { env = { XDG_STATE_HOME = xstate } }
|
||||
n.mkdir_p(xstate .. pathsep .. (is_os('win') and 'nvim-data' or 'nvim'))
|
||||
|
||||
t.mkdir('Xdir')
|
||||
t.mkdir('Xdir/Xsubdir')
|
||||
t.write_file('Xdir/Xfile.txt', [[hello, world]])
|
||||
|
||||
t.write_file(
|
||||
'Xfile',
|
||||
[[
|
||||
let g:foobar = 42
|
||||
]]
|
||||
)
|
||||
screen = Screen.new(500, 8)
|
||||
end)
|
||||
|
||||
teardown(function()
|
||||
after_each(function()
|
||||
screen:detach()
|
||||
os.remove('Xfile')
|
||||
n.rmdir('Xdir')
|
||||
n.rmdir(xstate)
|
||||
end)
|
||||
|
||||
it('works', function()
|
||||
local screen = Screen.new(500, 8)
|
||||
it('regular file', function()
|
||||
screen:set_default_attr_ids({
|
||||
[1] = { bold = true, foreground = Screen.colors.Blue1 },
|
||||
[2] = { bold = true, reverse = true },
|
||||
@ -94,7 +102,7 @@ describe('vim.secure', function()
|
||||
local hash = fn.sha256(assert(read_file('Xfile')))
|
||||
trust = assert(read_file(stdpath('state') .. pathsep .. 'trust'))
|
||||
eq(string.format('%s %s', hash, cwd .. pathsep .. 'Xfile'), vim.trim(trust))
|
||||
eq(vim.NIL, exec_lua([[vim.secure.read('Xfile')]]))
|
||||
eq('let g:foobar = 42\n', exec_lua([[return vim.secure.read('Xfile')]]))
|
||||
|
||||
os.remove(stdpath('state') .. pathsep .. 'trust')
|
||||
|
||||
@ -144,6 +152,114 @@ describe('vim.secure', function()
|
||||
pcall_err(command, 'write')
|
||||
eq(true, api.nvim_get_option_value('readonly', {}))
|
||||
end)
|
||||
|
||||
it('directory', function()
|
||||
screen:set_default_attr_ids({
|
||||
[1] = { bold = true, foreground = Screen.colors.Blue1 },
|
||||
[2] = { bold = true, reverse = true },
|
||||
[3] = { bold = true, foreground = Screen.colors.SeaGreen },
|
||||
[4] = { reverse = true },
|
||||
})
|
||||
|
||||
local cwd = fn.getcwd()
|
||||
local msg = cwd
|
||||
.. pathsep
|
||||
.. 'Xdir is not trusted. DIRECTORY trust is decided only by its name, not its contents.'
|
||||
if #msg >= screen._width then
|
||||
pending('path too long')
|
||||
return
|
||||
end
|
||||
|
||||
-- Need to use feed_command instead of exec_lua because of the confirmation prompt
|
||||
feed_command([[lua vim.secure.read('Xdir')]])
|
||||
screen:expect([[
|
||||
{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*3
|
||||
{2:{MATCH: +}}|
|
||||
:lua vim.secure.read('Xdir'){MATCH: +}|
|
||||
{3:]] .. msg .. [[}{MATCH: +}|
|
||||
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}|
|
||||
]])
|
||||
feed('d')
|
||||
screen:expect([[
|
||||
^{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*6
|
||||
{MATCH: +}|
|
||||
]])
|
||||
|
||||
local trust = assert(read_file(stdpath('state') .. pathsep .. 'trust'))
|
||||
eq(string.format('! %s', cwd .. pathsep .. 'Xdir'), vim.trim(trust))
|
||||
eq(vim.NIL, exec_lua([[return vim.secure.read('Xdir')]]))
|
||||
|
||||
os.remove(stdpath('state') .. pathsep .. 'trust')
|
||||
|
||||
feed_command([[lua vim.secure.read('Xdir')]])
|
||||
screen:expect([[
|
||||
{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*3
|
||||
{2:{MATCH: +}}|
|
||||
:lua vim.secure.read('Xdir'){MATCH: +}|
|
||||
{3:]] .. msg .. [[}{MATCH: +}|
|
||||
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}|
|
||||
]])
|
||||
feed('a')
|
||||
screen:expect([[
|
||||
^{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*6
|
||||
{MATCH: +}|
|
||||
]])
|
||||
|
||||
-- Directories aren't hashed in the trust database, instead a slug ("directory") is stored
|
||||
-- instead.
|
||||
local expected_hash = 'directory'
|
||||
trust = assert(read_file(stdpath('state') .. pathsep .. 'trust'))
|
||||
eq(string.format('%s %s', expected_hash, cwd .. pathsep .. 'Xdir'), vim.trim(trust))
|
||||
eq(true, exec_lua([[return vim.secure.read('Xdir')]]))
|
||||
|
||||
os.remove(stdpath('state') .. pathsep .. 'trust')
|
||||
|
||||
feed_command([[lua vim.secure.read('Xdir')]])
|
||||
screen:expect([[
|
||||
{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*3
|
||||
{2:{MATCH: +}}|
|
||||
:lua vim.secure.read('Xdir'){MATCH: +}|
|
||||
{3:]] .. msg .. [[}{MATCH: +}|
|
||||
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}|
|
||||
]])
|
||||
feed('i')
|
||||
screen:expect([[
|
||||
^{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*6
|
||||
{MATCH: +}|
|
||||
]])
|
||||
|
||||
-- Trust database is not updated
|
||||
eq(nil, read_file(stdpath('state') .. pathsep .. 'trust'))
|
||||
|
||||
feed_command([[lua vim.secure.read('Xdir')]])
|
||||
screen:expect([[
|
||||
{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*3
|
||||
{2:{MATCH: +}}|
|
||||
:lua vim.secure.read('Xdir'){MATCH: +}|
|
||||
{3:]] .. msg .. [[}{MATCH: +}|
|
||||
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^{MATCH: +}|
|
||||
]])
|
||||
feed('v')
|
||||
screen:expect([[
|
||||
^{MATCH: +}|
|
||||
{1:~{MATCH: +}}|*2
|
||||
{2:]] .. fn.fnamemodify(cwd, ':~') .. pathsep .. [[Xdir [RO]{MATCH: +}}|
|
||||
{MATCH: +}|
|
||||
{1:~{MATCH: +}}|
|
||||
{4:[No Name]{MATCH: +}}|
|
||||
{MATCH: +}|
|
||||
]])
|
||||
|
||||
-- Trust database is not updated
|
||||
eq(nil, read_file(stdpath('state') .. pathsep .. 'trust'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('trust()', function()
|
||||
@ -160,10 +276,12 @@ describe('vim.secure', function()
|
||||
|
||||
before_each(function()
|
||||
t.write_file('test_file', 'test')
|
||||
t.mkdir('test_dir')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
os.remove('test_file')
|
||||
n.rmdir('test_dir')
|
||||
end)
|
||||
|
||||
it('returns error when passing both path and bufnr', function()
|
||||
@ -275,5 +393,15 @@ describe('vim.secure', function()
|
||||
exec_lua([[return {vim.secure.trust({action='allow', bufnr=0})}]])
|
||||
)
|
||||
end)
|
||||
|
||||
it('trust directory bufnr', function()
|
||||
local cwd = fn.getcwd()
|
||||
local full_path = cwd .. pathsep .. 'test_dir'
|
||||
command('edit test_dir')
|
||||
|
||||
eq({ true, full_path }, exec_lua([[return {vim.secure.trust({action='allow', bufnr=0})}]]))
|
||||
local trust = read_file(stdpath('state') .. pathsep .. 'trust')
|
||||
eq(string.format('directory %s', full_path), vim.trim(trust))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
Reference in New Issue
Block a user