5809a843c8
Installing a plugin no longer needs unzip or tar on PATH, and entry paths are vetted in Lua instead of trusting an external tool. Add arti-pack.archive: a zip reader over zzlib's inflate and a tar reader over its gunzip. Reject absolute, parent-walking, backslashed and NUL-bearing entry names, and any entry that is not a regular file or directory. Verify every zip entry's CRC32 and size, and fail the install on mismatch. Carry only the executable bit from archive modes. Replace fs.extract_cmd with fs.extract and call it synchronously. Drop unzip from the test pipeline and the stated requirements.
208 lines
7.5 KiB
Lua
208 lines
7.5 KiB
Lua
local t = require('tests.harness')
|
|
local fs = require('arti-pack.fs')
|
|
local installer = require('arti-pack.installer')
|
|
local Lock = require('arti-pack.lock')
|
|
local spec_mod = require('arti-pack.spec')
|
|
|
|
local function tmpdir()
|
|
local dir = vim.fn.tempname()
|
|
vim.fn.mkdir(dir, 'p')
|
|
return dir
|
|
end
|
|
|
|
local function write(path, contents)
|
|
local fd = assert(io.open(path, 'wb'))
|
|
fd:write(contents)
|
|
fd:close()
|
|
return path
|
|
end
|
|
|
|
local function have(cmd)
|
|
return vim.fn.executable(cmd) == 1
|
|
end
|
|
|
|
-- Build a tag archive that looks like the real thing: everything wrapped in a
|
|
-- single "<repo>-<version>" directory.
|
|
local function build_archive(ext, repo, version)
|
|
local work = tmpdir()
|
|
local root = string.format('%s-%s', repo, version)
|
|
vim.fn.mkdir(work .. '/' .. root .. '/lua', 'p')
|
|
vim.fn.mkdir(work .. '/' .. root .. '/doc', 'p')
|
|
write(work .. '/' .. root .. '/lua/marker.lua', 'return "' .. version .. '"')
|
|
write(work .. '/' .. root .. '/doc/thing.txt', '*thing.txt*\tA help tag\n')
|
|
|
|
local archive = string.format('%s/%s.%s', tmpdir(), root, ext)
|
|
local cmd
|
|
if ext == 'zip' then
|
|
cmd = { 'zip', '-q', '-r', archive, root }
|
|
else
|
|
cmd = { 'tar', '-czf', archive, root }
|
|
end
|
|
local out = vim.system(cmd, { cwd = work, text = true }):wait()
|
|
assert(out.code == 0, 'failed to build fixture archive: ' .. tostring(out.stderr))
|
|
return archive
|
|
end
|
|
|
|
local function new_ctx()
|
|
local base = tmpdir()
|
|
return {
|
|
registry = 'file://unused',
|
|
pack_dir = fs.mkdirp(base .. '/pack/arti-pack'),
|
|
cache_dir = fs.mkdirp(base .. '/cache'),
|
|
concurrency = 2,
|
|
timeout = 30,
|
|
lock = Lock.new(base .. '/lock.json'),
|
|
}
|
|
end
|
|
|
|
local function run(ctx, specs)
|
|
local results
|
|
installer.run(ctx, specs, function(r)
|
|
results = r
|
|
end)
|
|
assert(
|
|
vim.wait(30000, function()
|
|
return results ~= nil
|
|
end, 20),
|
|
'installer.run never called back'
|
|
)
|
|
return results
|
|
end
|
|
|
|
local function spec_for(source, tag, archive, extra)
|
|
local entry = vim.tbl_extend('force', { source, tag = tag, url = 'file://' .. archive }, extra or {})
|
|
return assert(spec_mod.parse(entry))
|
|
end
|
|
|
|
t.describe('installer.run', function()
|
|
if not have('zip') or not have('tar') then
|
|
t.it('SKIPPED: zip/tar unavailable to build fixtures', function() end)
|
|
return
|
|
end
|
|
|
|
t.it('installs a zip into pack/start and strips the wrapper directory', function()
|
|
local archive = build_archive('zip', 'plenary.nvim', '0.1.4')
|
|
local ctx = new_ctx()
|
|
local results = run(ctx, { spec_for('nvim-lua/plenary.nvim', 'v0.1.4', archive) })
|
|
|
|
t.eq(1, #results)
|
|
t.eq('installed', results[1].status, results[1].err)
|
|
|
|
local dir = ctx.pack_dir .. '/start/plenary.nvim'
|
|
t.truthy(fs.exists(dir .. '/lua/marker.lua'), 'plugin files land at the top level')
|
|
t.falsy(fs.exists(dir .. '/plenary.nvim-0.1.4'), 'wrapper directory is stripped')
|
|
t.eq('v0.1.4', ctx.lock:get('plenary.nvim').tag)
|
|
end)
|
|
|
|
t.it('installs into pack/opt when opt is set', function()
|
|
local archive = build_archive('zip', 'lazy.nvim', '1.0.0')
|
|
local ctx = new_ctx()
|
|
local results = run(ctx, { spec_for('folke/lazy.nvim', 'v1.0.0', archive, { opt = true }) })
|
|
|
|
t.eq('installed', results[1].status, results[1].err)
|
|
t.truthy(fs.exists(ctx.pack_dir .. '/opt/lazy.nvim/lua/marker.lua'))
|
|
t.falsy(fs.exists(ctx.pack_dir .. '/start/lazy.nvim'))
|
|
t.eq(true, ctx.lock:get('lazy.nvim').opt)
|
|
end)
|
|
|
|
t.it('installs a tar.gz the same way', function()
|
|
local archive = build_archive('tar.gz', 'thing.nvim', '2.0.0')
|
|
local ctx = new_ctx()
|
|
local results = run(ctx, { spec_for('owner/thing.nvim', 'v2.0.0', archive, { ext = 'tar.gz' }) })
|
|
|
|
t.eq('installed', results[1].status, results[1].err)
|
|
t.truthy(fs.exists(ctx.pack_dir .. '/start/thing.nvim/lua/marker.lua'))
|
|
end)
|
|
|
|
t.it('replaces the old tree on a tag change and reports an update', function()
|
|
local ctx = new_ctx()
|
|
run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', build_archive('zip', 'thing.nvim', '1.0.0')) })
|
|
write(ctx.pack_dir .. '/start/thing.nvim/leftover.txt', 'stale')
|
|
|
|
local results = run(ctx, { spec_for('owner/thing.nvim', 'v2.0.0', build_archive('zip', 'thing.nvim', '2.0.0')) })
|
|
t.eq('updated', results[1].status, results[1].err)
|
|
t.falsy(fs.exists(ctx.pack_dir .. '/start/thing.nvim/leftover.txt'), 'stale files are cleared')
|
|
t.eq('v2.0.0', ctx.lock:get('thing.nvim').tag)
|
|
end)
|
|
|
|
t.it('moves a plugin between start and opt without leaving a copy behind', function()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
local ctx = new_ctx()
|
|
run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', archive) })
|
|
run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', archive, { opt = true }) })
|
|
|
|
t.truthy(fs.exists(ctx.pack_dir .. '/opt/thing.nvim'))
|
|
t.falsy(fs.exists(ctx.pack_dir .. '/start/thing.nvim'))
|
|
end)
|
|
|
|
t.it('fails one plugin without abandoning the rest of the run', function()
|
|
local ctx = new_ctx()
|
|
local results = run(ctx, {
|
|
spec_for('owner/good', 'v1.0.0', build_archive('zip', 'good', '1.0.0')),
|
|
spec_for('owner/missing', 'v1.0.0', tmpdir() .. '/does-not-exist.zip'),
|
|
spec_for('owner/alsogood', 'v1.0.0', build_archive('zip', 'alsogood', '1.0.0')),
|
|
})
|
|
|
|
local byname = {}
|
|
for _, r in ipairs(results) do
|
|
byname[r.name] = r
|
|
end
|
|
t.eq(3, #results)
|
|
t.eq('installed', byname.good.status)
|
|
t.eq('installed', byname.alsogood.status)
|
|
t.eq('failed', byname.missing.status)
|
|
t.matches('download failed', byname.missing.err)
|
|
t.falsy(ctx.lock:get('missing'), 'a failed plugin is not recorded')
|
|
end)
|
|
|
|
t.it('rejects a download that is not an archive and leaves nothing behind', function()
|
|
local ctx = new_ctx()
|
|
local bogus = write(tmpdir() .. '/bogus.zip', '<html>404 not found</html>')
|
|
local results = run(ctx, { spec_for('owner/bogus', 'v1.0.0', bogus) })
|
|
|
|
t.eq('failed', results[1].status)
|
|
t.matches('not a zip', results[1].err)
|
|
t.falsy(fs.exists(ctx.pack_dir .. '/start/bogus'))
|
|
end)
|
|
|
|
t.it('reuses a cached archive instead of downloading again', function()
|
|
local ctx = new_ctx()
|
|
local archive = build_archive('zip', 'cached.nvim', '1.0.0')
|
|
run(ctx, { spec_for('owner/cached.nvim', 'v1.0.0', archive) })
|
|
t.truthy(fs.exists(ctx.cache_dir .. '/cached.nvim-v1.0.0.zip'), 'archive is cached by name and tag')
|
|
|
|
-- Delete the source the url points at: a second run can only succeed from cache.
|
|
vim.fn.delete(archive)
|
|
fs.rm_rf(ctx.pack_dir .. '/start/cached.nvim')
|
|
local results = run(ctx, { spec_for('owner/cached.nvim', 'v1.0.0', archive) })
|
|
t.eq('updated', results[1].status, results[1].err)
|
|
t.truthy(fs.exists(ctx.pack_dir .. '/start/cached.nvim/lua/marker.lua'))
|
|
end)
|
|
|
|
t.it('installs several plugins concurrently', function()
|
|
local ctx = new_ctx()
|
|
local specs = {}
|
|
for i = 1, 5 do
|
|
local name = 'plug' .. i
|
|
table.insert(specs, spec_for('owner/' .. name, 'v1.0.0', build_archive('zip', name, '1.0.0')))
|
|
end
|
|
local results = run(ctx, specs)
|
|
|
|
t.eq(5, #results)
|
|
for _, r in ipairs(results) do
|
|
t.eq('installed', r.status, r.err)
|
|
t.truthy(fs.exists(ctx.pack_dir .. '/start/' .. r.name .. '/lua/marker.lua'))
|
|
end
|
|
end)
|
|
|
|
t.it('returns immediately for an empty spec list', function()
|
|
t.eq({}, run(new_ctx(), {}))
|
|
end)
|
|
|
|
t.it('leaves no staging directory behind', function()
|
|
local ctx = new_ctx()
|
|
run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', build_archive('zip', 'thing.nvim', '1.0.0')) })
|
|
t.eq({}, fs.entries(ctx.pack_dir .. '/.stage'))
|
|
end)
|
|
end)
|