ca55c9dd9f
- Reject archive members that are absolute, escape via .., are special files, or link outside the destination, before anything is written - Stage under the pack root so the swap is a same-filesystem rename, with an EXDEV copy fallback; propagate registry auth errors and validate dir_name
100 lines
3.2 KiB
Lua
100 lines
3.2 KiB
Lua
local M = {}
|
|
|
|
M.defaults = {
|
|
registry = nil,
|
|
plugins = {},
|
|
concurrency = 4,
|
|
timeout = 120000,
|
|
}
|
|
|
|
local function normalize_registry(registry, where)
|
|
if registry == nil then
|
|
return nil
|
|
end
|
|
if type(registry) ~= 'table' then
|
|
return nil, string.format('%s: registry must be a table', where)
|
|
end
|
|
if registry.auth and registry.auth.token ~= nil then
|
|
return nil, string.format('%s: registry auth must not carry a literal token; use env or token_cmd', where)
|
|
end
|
|
return registry
|
|
end
|
|
|
|
--- Validate and canonicalise user options into a config table.
|
|
--- Every plugin ends up with name, version, opt, dir_name and its own registry.
|
|
function M.normalize(opts)
|
|
opts = opts or {}
|
|
local cfg = vim.tbl_extend('force', vim.deepcopy(M.defaults), opts)
|
|
|
|
local default_registry, err = normalize_registry(cfg.registry, 'setup')
|
|
if err then
|
|
return nil, err
|
|
end
|
|
cfg.registry = default_registry
|
|
|
|
local seen = {}
|
|
local plugins = {}
|
|
for i, entry in ipairs(opts.plugins or {}) do
|
|
local spec = entry
|
|
if type(spec) == 'string' then
|
|
spec = { name = spec }
|
|
end
|
|
if type(spec) ~= 'table' then
|
|
return nil, string.format('plugins[%d]: expected table or string, got %s', i, type(entry))
|
|
end
|
|
spec = vim.deepcopy(spec)
|
|
if type(spec.name) ~= 'string' or spec.name == '' then
|
|
return nil, string.format('plugins[%d]: name is required', i)
|
|
end
|
|
if seen[spec.name] then
|
|
return nil, string.format('plugins[%d]: duplicate plugin name %q', i, spec.name)
|
|
end
|
|
seen[spec.name] = true
|
|
if not spec.url and (type(spec.version) ~= 'string' or spec.version == '') then
|
|
return nil, string.format('plugin %q: version is required unless url is given', spec.name)
|
|
end
|
|
local reg, rerr = normalize_registry(spec.registry, string.format('plugin %q', spec.name))
|
|
if rerr then
|
|
return nil, rerr
|
|
end
|
|
spec.registry = reg or cfg.registry
|
|
if not spec.registry and not spec.url then
|
|
return nil, string.format('plugin %q: no registry configured and no url given', spec.name)
|
|
end
|
|
spec.opt = spec.opt == true
|
|
spec.version = spec.version or 'url'
|
|
local slug = require('vimpack.util').slug
|
|
if spec.dir_name ~= nil then
|
|
if type(spec.dir_name) ~= 'string' or spec.dir_name == '' then
|
|
return nil, string.format('plugin %q: dir_name must be a non-empty string', spec.name)
|
|
end
|
|
-- A dir_name is joined onto the pack root, so it must be a single
|
|
-- component: anything slug() would rewrite, plus "." and "..", is out.
|
|
if spec.dir_name == '.' or spec.dir_name == '..' or spec.dir_name ~= slug(spec.dir_name) then
|
|
return nil,
|
|
string.format('plugin %q: dir_name %q must not contain path separators or ".."', spec.name, spec.dir_name)
|
|
end
|
|
end
|
|
spec.dir_name = spec.dir_name or slug(spec.name)
|
|
plugins[#plugins + 1] = spec
|
|
end
|
|
cfg.plugins = plugins
|
|
|
|
if type(cfg.concurrency) ~= 'number' or cfg.concurrency < 1 then
|
|
return nil, 'concurrency must be a positive number'
|
|
end
|
|
|
|
return cfg
|
|
end
|
|
|
|
--- Index plugins by name for O(1) lookup.
|
|
function M.by_name(cfg)
|
|
local out = {}
|
|
for _, spec in ipairs(cfg.plugins) do
|
|
out[spec.name] = spec
|
|
end
|
|
return out
|
|
end
|
|
|
|
return M
|