Files
vimpack/tests/fs_spec.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

272 lines
9.7 KiB
Lua

local fsmod = require('vimpack.fs')
describe('scan', function()
it('maps directories to their bucket', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/start/a/lua', 'p')
vim.fn.mkdir(root .. '/opt/b', 'p')
vim.fn.writefile({ '' }, root .. '/start/loose.txt')
local found = fsmod.scan(root)
assert_eq(false, found.a.opt)
assert_eq(true, found.b.opt)
assert_nil(found['loose.txt'])
end)
it('returns nothing for a missing root', function()
assert_eq({}, fsmod.scan(tmpdir() .. '/absent'))
end)
end)
describe('target_dir', function()
it('splits start and opt', function()
assert_eq('/r/start/a', fsmod.target_dir({ name = 'a', dir_name = 'a', opt = false }, '/r'))
assert_eq('/r/opt/a', fsmod.target_dir({ name = 'a', dir_name = 'a', opt = true }, '/r'))
end)
end)
describe('strip_top_level', function()
it('hoists a single wrapper directory', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/oil.nvim-2.11.0/lua', 'p')
assert_eq(root .. '/oil.nvim-2.11.0', fsmod.strip_top_level(root))
end)
it('leaves a flat archive alone', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/lua', 'p')
vim.fn.mkdir(root .. '/plugin', 'p')
assert_eq(root, fsmod.strip_top_level(root))
end)
end)
describe('swap', function()
it('replaces an existing install', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/target', 'p')
vim.fn.writefile({ 'old' }, root .. '/target/marker')
vim.fn.mkdir(root .. '/staged', 'p')
vim.fn.writefile({ 'new' }, root .. '/staged/marker')
assert_true(fsmod.swap(root .. '/staged', root .. '/target'))
assert_eq({ 'new' }, vim.fn.readfile(root .. '/target/marker'))
assert_true(not fsmod.exists(root .. '/target.vimpack-old'))
end)
it('keeps the previous install when the staged tree is missing', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/target', 'p')
vim.fn.writefile({ 'old' }, root .. '/target/marker')
local ok, err = fsmod.swap(root .. '/absent', root .. '/target')
assert_true(not ok)
assert_match('cannot move new install into place', err)
assert_eq({ 'old' }, vim.fn.readfile(root .. '/target/marker'))
end)
-- Forced EXDEV: deterministic everywhere, no second mount required.
local function with_exdev_on(path, fn)
local uv = vim.uv or vim.loop
local real = uv.fs_rename
uv.fs_rename = function(from, to)
if from == path then
return nil, 'EXDEV: cross-device link not permitted: ' .. from .. ' -> ' .. to, 'EXDEV'
end
return real(from, to)
end
local ok, err = pcall(fn)
uv.fs_rename = real
if not ok then
error(err, 0)
end
end
it('falls back to a copy when staged and target are on different filesystems', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/target', 'p')
vim.fn.writefile({ 'old' }, root .. '/target/marker')
vim.fn.mkdir(root .. '/staged/lua', 'p')
vim.fn.writefile({ 'new' }, root .. '/staged/lua/plug.lua')
with_exdev_on(root .. '/staged', function()
assert_true(fsmod.swap(root .. '/staged', root .. '/target'))
end)
assert_eq({ 'new' }, vim.fn.readfile(root .. '/target/lua/plug.lua'))
assert_true(not fsmod.exists(root .. '/target/marker'))
assert_true(not fsmod.exists(root .. '/target.vimpack-old'))
assert_true(not fsmod.exists(root .. '/target.vimpack-new'))
assert_true(not fsmod.exists(root .. '/staged'))
end)
it('restores the previous install when the cross-filesystem copy fails', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/target', 'p')
vim.fn.writefile({ 'old' }, root .. '/target/marker')
local ok, err
with_exdev_on(root .. '/absent', function()
ok, err = fsmod.swap(root .. '/absent', root .. '/target')
end)
assert_true(not ok)
assert_match('cannot move new install into place', err)
assert_eq({ 'old' }, vim.fn.readfile(root .. '/target/marker'))
assert_true(not fsmod.exists(root .. '/target.vimpack-new'))
end)
it('copies a tree across a real filesystem boundary', function()
local root = tmpdir()
local other = otherfs_dir(root)
if not other then
return
end
vim.fn.mkdir(root .. '/staged/lua', 'p')
vim.fn.writefile({ 'new' }, root .. '/staged/lua/plug.lua')
vim.fn.mkdir(root .. '/staged/empty', 'p')
assert_true(fsmod.copy_tree(root .. '/staged', other .. '/copy'))
assert_eq({ 'new' }, vim.fn.readfile(other .. '/copy/lua/plug.lua'))
assert_true(fsmod.exists(other .. '/copy/empty'))
fsmod.rm_rf(other)
end)
end)
describe('safe_relative', function()
it('accepts a plain relative path', function()
assert_eq('lua/plug.lua', fsmod.safe_relative('./lua/plug.lua'))
assert_eq('lua', fsmod.safe_relative('lua/sub/..'))
end)
it('rejects absolute paths and escapes', function()
assert_nil(fsmod.safe_relative('/etc/passwd'))
assert_nil(fsmod.safe_relative('../evil'))
assert_nil(fsmod.safe_relative('a/../../evil'))
assert_nil(fsmod.safe_relative(''))
end)
end)
describe('extract', function()
local function wait_extract(archive, dest)
local result
fsmod.extract(archive, dest, function(ok, err)
result = { ok = ok, err = err }
end)
vim.wait(10000, function()
return result ~= nil
end)
return result
end
it('unpacks a tarball', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
vim.fn.system({ 'tar', '-czf', root .. '/plug.tar.gz', '-C', root .. '/src', 'plug' })
local result = wait_extract(root .. '/plug.tar.gz', root .. '/dest')
assert_true(result.ok, tostring(result.err))
assert_true(fsmod.exists(root .. '/dest/plug/lua/plug.lua'))
assert_eq(root .. '/dest/plug', fsmod.strip_top_level(root .. '/dest'))
end)
it('rejects an unknown archive type', function()
local result = wait_extract(tmpdir() .. '/thing.rpm', tmpdir() .. '/dest')
assert_true(not result.ok)
assert_match('unsupported archive type', result.err)
end)
it('reports a corrupt tarball', function()
local root = tmpdir()
vim.fn.writefile({ 'definitely not a tarball' }, root .. '/bad.tar.gz')
local result = wait_extract(root .. '/bad.tar.gz', root .. '/dest')
assert_true(not result.ok)
assert_match('extraction failed', result.err)
end)
it('unpacks a zip', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
vim.fn.system({ 'sh', '-c', 'cd ' .. vim.fn.shellescape(root .. '/src') .. ' && zip -qr ../plug.zip plug' })
if vim.v.shell_error ~= 0 then
return
end
local result = wait_extract(root .. '/plug.zip', root .. '/dest')
assert_true(result.ok, tostring(result.err))
assert_true(fsmod.exists(root .. '/dest/plug/lua/plug.lua'))
end)
it('refuses a tarball that climbs out of the destination', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug', 'p')
vim.fn.writefile({ 'pwned' }, root .. '/src/plug/payload')
vim.fn.system({
'tar',
'-czf',
root .. '/evil.tar.gz',
'-C',
root .. '/src',
'--transform',
's|^plug|../evil|',
'plug/payload',
})
local result = wait_extract(root .. '/evil.tar.gz', root .. '/dest')
assert_true(not result.ok)
assert_match('refusing to extract', result.err)
assert_match('../evil/payload', result.err)
assert_true(not fsmod.exists(root .. '/evil'))
assert_true(not fsmod.exists(root .. '/dest/payload'))
end)
it('refuses a tarball holding an absolute path', function()
local root = tmpdir()
local victim = root .. '/abs-evil'
vim.fn.writefile({ 'pwned' }, victim)
vim.fn.system({ 'tar', '-cPzf', root .. '/abs.tar.gz', victim })
local result = wait_extract(root .. '/abs.tar.gz', root .. '/dest')
assert_true(not result.ok)
assert_match('refusing to extract', result.err)
assert_match('is absolute or escapes the destination directory', result.err)
assert_true(not fsmod.exists(root .. '/dest'))
end)
it('refuses a tarball whose symlink points outside the destination', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug', 'p')
vim.fn.system({ 'ln', '-s', '/etc/passwd', root .. '/src/plug/escape' })
vim.fn.system({ 'tar', '-czf', root .. '/link.tar.gz', '-C', root .. '/src', 'plug' })
local result = wait_extract(root .. '/link.tar.gz', root .. '/dest')
assert_true(not result.ok)
assert_match('refusing to extract', result.err)
assert_match('plug/escape', result.err)
assert_true(not fsmod.exists(root .. '/dest/plug'))
end)
it('refuses a tarball whose symlink climbs out with ..', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
vim.fn.system({ 'ln', '-s', '../../../outside', root .. '/src/plug/lua/escape' })
vim.fn.system({ 'tar', '-czf', root .. '/rel.tar.gz', '-C', root .. '/src', 'plug' })
local result = wait_extract(root .. '/rel.tar.gz', root .. '/dest')
assert_true(not result.ok)
assert_match('refusing to extract', result.err)
assert_match('plug/lua/escape', result.err)
end)
it('allows a symlink that stays inside the destination', function()
local root = tmpdir()
vim.fn.mkdir(root .. '/src/plug/lua', 'p')
vim.fn.writefile({ 'return {}' }, root .. '/src/plug/lua/plug.lua')
vim.fn.system({ 'ln', '-s', 'lua/plug.lua', root .. '/src/plug/alias' })
vim.fn.system({ 'tar', '-czf', root .. '/ok.tar.gz', '-C', root .. '/src', 'plug' })
local result = wait_extract(root .. '/ok.tar.gz', root .. '/dest')
assert_true(result.ok, tostring(result.err))
assert_true(fsmod.exists(root .. '/dest/plug/alias'))
end)
end)