secure.lua 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. local M = {}
  2. --- Reads trust database from $XDG_STATE_HOME/nvim/trust.
  3. ---
  4. ---@return table<string, string> Contents of trust database, if it exists. Empty table otherwise.
  5. local function read_trust()
  6. local trust = {} ---@type table<string, string>
  7. local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
  8. if f then
  9. local contents = f:read('*a')
  10. if contents then
  11. for line in vim.gsplit(contents, '\n') do
  12. local hash, file = string.match(line, '^(%S+) (.+)$')
  13. if hash and file then
  14. trust[file] = hash
  15. end
  16. end
  17. end
  18. f:close()
  19. end
  20. return trust
  21. end
  22. --- Writes provided {trust} table to trust database at
  23. --- $XDG_STATE_HOME/nvim/trust.
  24. ---
  25. ---@param trust table<string, string> Trust table to write
  26. local function write_trust(trust)
  27. vim.validate('trust', trust, 'table')
  28. local f = assert(io.open(vim.fn.stdpath('state') .. '/trust', 'w'))
  29. local t = {} ---@type string[]
  30. for p, h in pairs(trust) do
  31. t[#t + 1] = string.format('%s %s\n', h, p)
  32. end
  33. f:write(table.concat(t))
  34. f:close()
  35. end
  36. --- Attempt to read the file at {path} prompting the user if the file should be
  37. --- trusted. The user's choice is persisted in a trust database at
  38. --- $XDG_STATE_HOME/nvim/trust.
  39. ---
  40. ---@since 11
  41. ---@see |:trust|
  42. ---
  43. ---@param path (string) Path to a file to read.
  44. ---
  45. ---@return (string|nil) The contents of the given file if it exists and is
  46. --- trusted, or nil otherwise.
  47. function M.read(path)
  48. vim.validate('path', path, 'string')
  49. local fullpath = vim.uv.fs_realpath(vim.fs.normalize(path))
  50. if not fullpath then
  51. return nil
  52. end
  53. local trust = read_trust()
  54. if trust[fullpath] == '!' then
  55. -- File is denied
  56. return nil
  57. end
  58. local contents ---@type string?
  59. do
  60. local f = io.open(fullpath, 'r')
  61. if not f then
  62. return nil
  63. end
  64. contents = f:read('*a')
  65. f:close()
  66. end
  67. local hash = vim.fn.sha256(contents)
  68. if trust[fullpath] == hash then
  69. -- File already exists in trust database
  70. return contents
  71. end
  72. -- File either does not exist in trust database or the hash does not match
  73. local ok, result = pcall(
  74. vim.fn.confirm,
  75. string.format('%s is not trusted.', fullpath),
  76. '&ignore\n&view\n&deny\n&allow',
  77. 1
  78. )
  79. if not ok and result ~= 'Keyboard interrupt' then
  80. error(result)
  81. elseif not ok or result == 0 or result == 1 then
  82. -- Cancelled or ignored
  83. return nil
  84. elseif result == 2 then
  85. -- View
  86. vim.cmd('sview ' .. fullpath)
  87. return nil
  88. elseif result == 3 then
  89. -- Deny
  90. trust[fullpath] = '!'
  91. contents = nil
  92. elseif result == 4 then
  93. -- Allow
  94. trust[fullpath] = hash
  95. end
  96. write_trust(trust)
  97. return contents
  98. end
  99. --- @class vim.trust.opts
  100. --- @inlinedoc
  101. ---
  102. --- - `'allow'` to add a file to the trust database and trust it,
  103. --- - `'deny'` to add a file to the trust database and deny it,
  104. --- - `'remove'` to remove file from the trust database
  105. --- @field action 'allow'|'deny'|'remove'
  106. ---
  107. --- Path to a file to update. Mutually exclusive with {bufnr}.
  108. --- Cannot be used when {action} is "allow".
  109. --- @field path? string
  110. --- Buffer number to update. Mutually exclusive with {path}.
  111. --- @field bufnr? integer
  112. --- Manage the trust database.
  113. ---
  114. --- The trust database is located at |$XDG_STATE_HOME|/nvim/trust.
  115. ---
  116. ---@since 11
  117. ---@param opts vim.trust.opts
  118. ---@return boolean success true if operation was successful
  119. ---@return string msg full path if operation was successful, else error message
  120. function M.trust(opts)
  121. vim.validate('path', opts.path, 'string', true)
  122. vim.validate('bufnr', opts.bufnr, 'number', true)
  123. vim.validate('action', opts.action, function(m)
  124. return m == 'allow' or m == 'deny' or m == 'remove'
  125. end, [["allow" or "deny" or "remove"]])
  126. ---@cast opts vim.trust.opts
  127. local path = opts.path
  128. local bufnr = opts.bufnr
  129. local action = opts.action
  130. assert(not path or not bufnr, '"path" and "bufnr" are mutually exclusive')
  131. if action == 'allow' then
  132. assert(not path, '"path" is not valid when action is "allow"')
  133. end
  134. local fullpath ---@type string?
  135. if path then
  136. fullpath = vim.uv.fs_realpath(vim.fs.normalize(path))
  137. elseif bufnr then
  138. local bufname = vim.api.nvim_buf_get_name(bufnr)
  139. if bufname == '' then
  140. return false, 'buffer is not associated with a file'
  141. end
  142. fullpath = vim.uv.fs_realpath(vim.fs.normalize(bufname))
  143. else
  144. error('one of "path" or "bufnr" is required')
  145. end
  146. if not fullpath then
  147. return false, string.format('invalid path: %s', path)
  148. end
  149. local trust = read_trust()
  150. if action == 'allow' then
  151. local newline = vim.bo[bufnr].fileformat == 'unix' and '\n' or '\r\n'
  152. local contents =
  153. table.concat(vim.api.nvim_buf_get_lines(bufnr --[[@as integer]], 0, -1, false), newline)
  154. if vim.bo[bufnr].endofline then
  155. contents = contents .. newline
  156. end
  157. local hash = vim.fn.sha256(contents)
  158. trust[fullpath] = hash
  159. elseif action == 'deny' then
  160. trust[fullpath] = '!'
  161. elseif action == 'remove' then
  162. trust[fullpath] = nil
  163. end
  164. write_trust(trust)
  165. return true, fullpath
  166. end
  167. return M