Files
vimpack/lua/vimpack/config.lua
T
unkin-agent ca55c9dd9f
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Validate archive entries and stage inside the pack root
- 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
2026-09-05 16:32:47 +10:00

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