aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/secure.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
commit9243becbedbb6a1592208051f8fa2b090dcc5e7d (patch)
tree607c2a862ec3f4399b8766383f6f8e04c4aa43b4 /runtime/lua/vim/secure.lua
parent9e40b6e9e1bc67f2d856adb837ee64dd0e25b717 (diff)
parent3c48d3c83fc21dbc0841f9210f04bdb073d73cd1 (diff)
downloadrneovim-9243becbedbb6a1592208051f8fa2b090dcc5e7d.tar.gz
rneovim-9243becbedbb6a1592208051f8fa2b090dcc5e7d.tar.bz2
rneovim-9243becbedbb6a1592208051f8fa2b090dcc5e7d.zip
Merge remote-tracking branch 'upstream/master' into usermarksusermarks
Diffstat (limited to 'runtime/lua/vim/secure.lua')
-rw-r--r--runtime/lua/vim/secure.lua188
1 files changed, 188 insertions, 0 deletions
diff --git a/runtime/lua/vim/secure.lua b/runtime/lua/vim/secure.lua
new file mode 100644
index 0000000000..443b152273
--- /dev/null
+++ b/runtime/lua/vim/secure.lua
@@ -0,0 +1,188 @@
+local M = {}
+
+---@private
+--- Reads trust database from $XDG_STATE_HOME/nvim/trust.
+---
+---@return (table) Contents of trust database, if it exists. Empty table otherwise.
+local function read_trust()
+ local trust = {}
+ 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
+
+---@private
+--- Writes provided {trust} table to trust database at
+--- $XDG_STATE_HOME/nvim/trust.
+---
+---@param trust (table) Trust table to write
+local function write_trust(trust)
+ vim.validate({ trust = { trust, 't' } })
+ local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w'))
+
+ local t = {}
+ 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
+
+--- 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
+--- $XDG_STATE_HOME/nvim/trust.
+---
+---@see |:trust|
+---
+---@param path (string) Path to a file to read.
+---
+---@return (string|nil) The contents of the given file if it exists and is
+--- trusted, or nil otherwise.
+function M.read(path)
+ vim.validate({ path = { path, 's' } })
+ local fullpath = vim.loop.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
+ do
+ local f = io.open(fullpath, 'r')
+ if not f then
+ return nil
+ end
+ contents = f:read('*a')
+ f:close()
+ end
+
+ local hash = vim.fn.sha256(contents)
+ if trust[fullpath] == hash then
+ -- File already exists in trust database
+ return 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),
+ '&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
+
+--- Manage the trust database.
+---
+--- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
+---
+---@param opts (table):
+--- - action (string): "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
+--- - path (string|nil): Path to a file to update. Mutually exclusive with {bufnr}.
+--- Cannot be used when {action} is "allow".
+--- - bufnr (number|nil): Buffer number to update. Mutually exclusive with {path}.
+---@return (boolean, string) success, msg:
+--- - true and full path of target file if operation was successful
+--- - false and error message on failure
+function M.trust(opts)
+ vim.validate({
+ path = { opts.path, 's', true },
+ bufnr = { opts.bufnr, 'n', true },
+ action = {
+ opts.action,
+ function(m)
+ return m == 'allow' or m == 'deny' or m == 'remove'
+ end,
+ [["allow" or "deny" or "remove"]],
+ },
+ })
+
+ 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
+ if path then
+ fullpath = vim.loop.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.loop.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 newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
+ local contents = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), newline)
+ if vim.bo[bufnr].endofline then
+ contents = contents .. newline
+ end
+ local hash = vim.fn.sha256(contents)
+
+ 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