fix(trust): support for trusting directories #33617

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.
This commit is contained in:
Jeremy Fleischman
2025-04-30 04:20:39 -07:00
committed by GitHub
parent 0ab0cdb2da
commit 272dba7f07
4 changed files with 213 additions and 31 deletions

View File

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