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
272 lines
9.7 KiB
Lua
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)
|