1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
|
local M = {}
--- Reads trust database from $XDG_STATE_HOME/nvim/trust.
---
---@return table<string, string> Contents of trust database, if it exists. Empty table otherwise.
local function read_trust()
local trust = {} ---@type table<string, string>
local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
if f then
local contents = f:read('*a')
if contents then
for line in vim.gsplit(contents, '\n') do
local hash, file = string.match(line, '^(%S+) (.+)$')
if hash and file then
trust[file] = hash
end
end
end
f:close()
end
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.
---
---@param trust table<string, string> Trust table to write
local function write_trust(trust)
vim.validate('trust', trust, 'table')
local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w'))
local t = {} ---@type string[]
for p, h in pairs(trust) do
t[#t + 1] = string.format('%s %s\n', h, p)
end
f:write(table.concat(t))
f:close()
end
--- 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 or directory to read.
---
---@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))
if not fullpath then
return nil
end
local trust = read_trust()
if trust[fullpath] == '!' then
-- File is denied
return nil
end
local contents, hash = compute_hash(fullpath, nil)
if not contents then
return nil
end
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.%s', fullpath, dir_msg),
'&ignore\n&view\n&deny\n&allow',
1
)
if not ok and result ~= 'Keyboard interrupt' then
error(result)
elseif not ok or result == 0 or result == 1 then
-- Cancelled or ignored
return nil
elseif result == 2 then
-- View
vim.cmd('sview ' .. fullpath)
return nil
elseif result == 3 then
-- Deny
trust[fullpath] = '!'
contents = nil
elseif result == 4 then
-- Allow
trust[fullpath] = hash
end
write_trust(trust)
return contents
end
--- @class vim.trust.opts
--- @inlinedoc
---
--- - `'allow'` to add a file to the trust database and trust it,
--- - `'deny'` to add a file to the trust database and deny it,
--- - `'remove'` to remove file from the trust database
--- @field action 'allow'|'deny'|'remove'
---
--- Path to a file to update. Mutually exclusive with {bufnr}.
--- Cannot be used when {action} is "allow".
--- @field path? string
--- Buffer number to update. Mutually exclusive with {path}.
--- @field bufnr? integer
--- Manage the trust database.
---
--- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
---
---@since 11
---@param opts vim.trust.opts
---@return boolean success true if operation was successful
---@return string msg full path if operation was successful, else error message
function M.trust(opts)
vim.validate('path', opts.path, 'string', true)
vim.validate('bufnr', opts.bufnr, 'number', true)
vim.validate('action', opts.action, function(m)
return m == 'allow' or m == 'deny' or m == 'remove'
end, [["allow" or "deny" or "remove"]])
---@cast opts vim.trust.opts
local path = opts.path
local bufnr = opts.bufnr
local action = opts.action
assert(not path or not bufnr, '"path" and "bufnr" are mutually exclusive')
if action == 'allow' then
assert(not path, '"path" is not valid when action is "allow"')
end
local fullpath ---@type string?
if path then
fullpath = vim.uv.fs_realpath(vim.fs.normalize(path))
elseif bufnr then
local bufname = vim.api.nvim_buf_get_name(bufnr)
if bufname == '' then
return false, 'buffer is not associated with a file'
end
fullpath = vim.uv.fs_realpath(vim.fs.normalize(bufname))
else
error('one of "path" or "bufnr" is required')
end
if not fullpath then
return false, string.format('invalid path: %s', path)
end
local trust = read_trust()
if action == 'allow' then
local contents, hash = compute_hash(fullpath, bufnr)
if not contents then
return false, string.format('could not read path: %s', fullpath)
end
trust[fullpath] = hash
elseif action == 'deny' then
trust[fullpath] = '!'
elseif action == 'remove' then
trust[fullpath] = nil
end
write_trust(trust)
return true, fullpath
end
return M
|