From dc3b49d5a1327a48a23b5b29101fe0adbda466b4 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 10 Apr 2026 08:34:05 +0800 Subject: [PATCH 1/2] vim-patch:9.2.0325: runtime(tar): bug in zstd handling Problem: patch 9.2.0325: runtime(tar): bug in zstd handling Solution: use correct --zstd argument, separated from other arguments, rework testing framework (Aaron Burrow). The tar.vim plugin allows vim to read and manipulate zstd archives, but it had a bug that caused extraction attempts to fail. Specifically, if the archive has a .tar.zst or .tzst extension, then the code was generating invalid extraction commands that looked like this: tar --zstdpxf foo.tar.zst foo When they should be like this: tar --zstd -pxf foo.tar.zst foo This patch changes the flag manipulation logic so that --zstd isn't glued to pxf. The labor for this change was divided between ChatGPT 5.4 and me. ChatGPT 5.4 identified the issue (from a code scan?), and I wrote the patch and tested vim. related: vim/vim#19930 https://github.com/vim/vim/commit/00285c035aa7d92971bc50b7b124e1408dda53ad Note: tests need the next patch to pass in Nvim. Co-authored-by: Aaron Burrow --- runtime/autoload/tar.vim | 5 +- test/old/testdir/test_plugin_tar.vim | 158 +++++++++++++++++++-------- 2 files changed, 113 insertions(+), 50 deletions(-) diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim index b2187a7ebc..6b90a7d07c 100644 --- a/runtime/autoload/tar.vim +++ b/runtime/autoload/tar.vim @@ -20,6 +20,7 @@ " 2026 Feb 06 by Vim Project: consider 'nowrapscan' (#19333) " 2026 Feb 07 by Vim Project: make the path traversal detection more robust (#19341) " 2026 Apr 06 by Vim Project: fix bugs with lz4 support (#19925) +" 2026 Apr 09 by Vim Project: fix bugs with zstd support (#19930) " " Contains many ideas from Michael Toren's " @@ -686,7 +687,7 @@ fun! tar#Extract() endif elseif filereadable(tarbase.".tzst") - let extractcmd= substitute(extractcmd,"-","--zstd","") + let extractcmd= substitute(extractcmd,"-","--zstd -","") call system(extractcmd." ".shellescape(tarbase).".tzst ".shellescape(fname)) if v:shell_error != 0 call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tzst {fname}: failed!") @@ -695,7 +696,7 @@ fun! tar#Extract() endif elseif filereadable(tarbase.".tar.zst") - let extractcmd= substitute(extractcmd,"-","--zstd","") + let extractcmd= substitute(extractcmd,"-","--zstd -","") call system(extractcmd." ".shellescape(tarbase).".tar.zst ".shellescape(fname)) if v:shell_error != 0 call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.zst {fname}: failed!") diff --git a/test/old/testdir/test_plugin_tar.vim b/test/old/testdir/test_plugin_tar.vim index 719487bda4..1a841f75c4 100644 --- a/test/old/testdir/test_plugin_tar.vim +++ b/test/old/testdir/test_plugin_tar.vim @@ -151,56 +151,118 @@ func Test_tar_path_traversal_with_nowrapscan() bw! endfunc -func Test_tar_lz4_extract() - CheckExecutable lz4 - - call delete('X.txt') - call delete('Xarchive.tar') - call delete('Xarchive.tar.lz4') - call writefile(['hello'], 'X.txt') - call system('tar -cf Xarchive.tar X.txt') +func s:CreateTar(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar -C ' .. tempdir .. ' -cf ' .. a:outputdir .. '/' .. a:archivename .. ' X.txt') call assert_equal(0, v:shell_error) - - call system('lz4 -z Xarchive.tar Xarchive.tar.lz4') - call assert_equal(0, v:shell_error) - - call delete('X.txt') - call delete('Xarchive.tar') - defer delete('Xarchive.tar.lz4') - - e Xarchive.tar.lz4 - call assert_match('X.txt', getline(5)) - :5 - normal x - call assert_true(filereadable('X.txt')) - call assert_equal(['hello'], readfile('X.txt')) - call delete('X.txt') - bw! endfunc -func Test_tlz4_extract() - CheckExecutable lz4 - - call delete('X.txt') - call delete('Xarchive.tar') - call delete('Xarchive.tlz4') - call writefile(['goodbye'], 'X.txt') - call system('tar -cf Xarchive.tar X.txt') +func s:CreateTgz(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar -C ' .. tempdir .. ' -czf ' .. a:outputdir .. '/' .. a:archivename .. ' X.txt') call assert_equal(0, v:shell_error) - - call system('lz4 -z Xarchive.tar Xarchive.tlz4') - call assert_equal(0, v:shell_error) - - call delete('X.txt') - call delete('Xarchive.tar') - defer delete('Xarchive.tlz4') - - e Xarchive.tlz4 - call assert_match('X.txt', getline(5)) - :5 - normal x - call assert_true(filereadable('X.txt')) - call assert_equal(['goodbye'], readfile('X.txt')) - call delete('X.txt') - bw! +endfunc + +func s:CreateTbz(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar -C ' .. tempdir .. ' -cjf ' .. a:outputdir .. '/' .. a:archivename .. ' X.txt') + call assert_equal(0, v:shell_error) +endfunc + +func s:CreateTxz(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar -C ' .. tempdir .. ' -cJf ' .. a:outputdir .. '/' .. a:archivename .. ' X.txt') + call assert_equal(0, v:shell_error) +endfunc + +func s:CreateTzst(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar --zstd -C ' .. tempdir .. ' -cf ' .. a:outputdir .. '/' .. a:archivename .. ' X.txt') + call assert_equal(0, v:shell_error) +endfunc + +func s:CreateTlz4(archivename, content, outputdir) + let tempdir = tempname() + call mkdir(tempdir, 'R') + call writefile([a:content], tempdir .. '/X.txt') + call assert_true(filereadable(tempdir .. '/X.txt')) + call system('tar -C ' .. tempdir .. ' -cf ' .. tempdir .. '/Xarchive.tar X.txt') + call assert_equal(0, v:shell_error) + call assert_true(filereadable(tempdir .. '/Xarchive.tar')) + call system('lz4 -z ' .. tempdir .. '/Xarchive.tar ' .. a:outputdir .. '/' .. a:archivename) + call assert_equal(0, v:shell_error) +endfunc + +" XXX: Add test for .tar.bz3 +func Test_extraction() + let control = [ + \ #{create: function('s:CreateTar'), + \ archive: 'Xarchive.tar'}, + \ #{create: function('s:CreateTgz'), + \ archive: 'Xarchive.tgz'}, + \ #{create: function('s:CreateTgz'), + \ archive: 'Xarchive.tar.gz'}, + \ #{create: function('s:CreateTbz'), + \ archive: 'Xarchive.tbz'}, + \ #{create: function('s:CreateTbz'), + \ archive: 'Xarchive.tar.bz2'}, + \ #{create: function('s:CreateTxz'), + \ archive: 'Xarchive.txz'}, + \ #{create: function('s:CreateTxz'), + \ archive: 'Xarchive.tar.xz'}, + \ ] + + if executable('lz4') == 1 + eval control->add(#{ + \ create: function('s:CreateTlz4'), + \ archive: 'Xarchive.tar.lz4' + \ }) + eval control->add(#{ + \ create: function('s:CreateTlz4'), + \ archive: 'Xarchive.tlz4' + \ }) + endif + if executable('zstd') == 1 + eval control->add(#{ + \ create: function('s:CreateTzst'), + \ archive: 'Xarchive.tar.zst' + \ }) + eval control->add(#{ + \ create: function('s:CreateTzst'), + \ archive: 'Xarchive.tzst' + \ }) + endif + + for c in control + let dir = tempname() + call mkdir(dir, 'R') + call call(c.create, [c.archive, 'hello', dir]) + + call delete('X.txt') + execute 'edit ' .. dir .. '/' .. c.archive + call assert_match('X.txt', getline(5), 'line 5 wrong in archive: ' .. c.archive) + :5 + normal x + call assert_equal(0, v:shell_error, 'vshell error not 0') + call assert_true(filereadable('X.txt'), 'X.txt not readable for archive: ' .. c.archive) + call assert_equal(['hello'], readfile('X.txt'), 'X.txt wrong contents for archive: ' .. c.archive) + call delete('X.txt') + call delete(dir .. '/' .. c.archive) + bw! + endfor endfunc From ec8d3521177b8f75a1ffd92cd7c915a74aa18c87 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 10 Apr 2026 12:28:07 +0800 Subject: [PATCH 2/2] vim-patch:9.2.0326: runtime(tar): but with dotted path Problem: runtime(tar): but with dotted path Solution: Do not strip everything after the first dot (Aaron Burrow) tar#Extract was getting the extensionless basename by stripping away everything starting with the leftmost dot. So if a directory had a dot or the file had an 'extra' dot then the code did the wrong thing. For example, if it was given: /tmp/foo.bar/baz.tar.gz Then it would treat /tmp/foo as the extensionless basename, but it actually should have grabbed: /tmp/foo.bar/baz This patch fixes the issue by instead looking at the rightmost dot(s). This bug was discovered by ChatGPT 5.4. I wrote the patch and tested vim. closes: vim/vim#19930 https://github.com/vim/vim/commit/4a1bcc67b4b6fc2dfe564ab4faca490f8afac857 Co-authored-by: Aaron Burrow --- runtime/autoload/tar.vim | 82 +++++++++++++++------------- test/old/testdir/test_plugin_tar.vim | 50 +++++++++++++++++ 2 files changed, 93 insertions(+), 39 deletions(-) diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim index 6b90a7d07c..0ae657c0d6 100644 --- a/runtime/autoload/tar.vim +++ b/runtime/autoload/tar.vim @@ -21,6 +21,7 @@ " 2026 Feb 07 by Vim Project: make the path traversal detection more robust (#19341) " 2026 Apr 06 by Vim Project: fix bugs with lz4 support (#19925) " 2026 Apr 09 by Vim Project: fix bugs with zstd support (#19930) +" 2026 Apr 09 by Vim Project: fix bug with dotted filename (#19930) " " Contains many ideas from Michael Toren's " @@ -611,117 +612,120 @@ fun! tar#Extract() return endif - let tarball = expand("%") - let tarbase = substitute(tarball,'\..*$','','') - let extractcmd= s:WinPath(g:tar_extractcmd) - if filereadable(tarbase.".tar") - call system(extractcmd." ".shellescape(tarbase).".tar ".shellescape(fname)) + let tarball = expand("%") + if !filereadable(tarball) + let &report= repkeep + return + endif + + if tarball =~# "\.tar$" + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ". fname endif - elseif filereadable(tarbase.".tgz") + elseif tarball =~# "\.tgz$" let extractcmd= substitute(extractcmd,"-","-z","") - call system(extractcmd." ".shellescape(tarbase).".tgz ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tgz {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.gz") + elseif tarball =~# "\.tar\.gz$" let extractcmd= substitute(extractcmd,"-","-z","") - call system(extractcmd." ".shellescape(tarbase).".tar.gz ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.gz {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tbz") + elseif tarball =~# "\.tbz$" let extractcmd= substitute(extractcmd,"-","-j","") - call system(extractcmd." ".shellescape(tarbase).".tbz ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tbz {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.bz2") + elseif tarball =~# "\.tar\.bz2$" let extractcmd= substitute(extractcmd,"-","-j","") - call system(extractcmd." ".shellescape(tarbase).".tar.bz2 ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz2 {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.bz3") + elseif tarball =~# "\.tar\.bz3$" let extractcmd= substitute(extractcmd,"-","-j","") - call system(extractcmd." ".shellescape(tarbase).".tar.bz3 ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.bz3 {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".txz") + elseif tarball =~# "\.txz$" let extractcmd= substitute(extractcmd,"-","-J","") - call system(extractcmd." ".shellescape(tarbase).".txz ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.txz {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.xz") + elseif tarball =~# "\.tar\.xz$" let extractcmd= substitute(extractcmd,"-","-J","") - call system(extractcmd." ".shellescape(tarbase).".tar.xz ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.xz {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tzst") + elseif tarball =~# "\.tzst$" let extractcmd= substitute(extractcmd,"-","--zstd -","") - call system(extractcmd." ".shellescape(tarbase).".tzst ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tzst {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.zst") + elseif tarball =~# "\.tar\.zst$" let extractcmd= substitute(extractcmd,"-","--zstd -","") - call system(extractcmd." ".shellescape(tarbase).".tar.zst ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.zst {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tlz4") + elseif tarball =~# "\.tlz4$" if has("linux") let extractcmd= substitute(extractcmd,"-","-I lz4 -","") endif - call system(extractcmd." ".shellescape(tarbase).".tlz4 ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tlz4 {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif - elseif filereadable(tarbase.".tar.lz4") + elseif tarball =~# "\.tar\.lz4$" if has("linux") let extractcmd= substitute(extractcmd,"-","-I lz4 -","") endif - call system(extractcmd." ".shellescape(tarbase).".tar.lz4 ".shellescape(fname)) + call system(extractcmd." ".shellescape(tarball)." ".shellescape(fname)) if v:shell_error != 0 - call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarbase}.tar.lz4 {fname}: failed!") + call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else echo "***note*** successfully extracted ".fname endif diff --git a/test/old/testdir/test_plugin_tar.vim b/test/old/testdir/test_plugin_tar.vim index 1a841f75c4..89d0924da0 100644 --- a/test/old/testdir/test_plugin_tar.vim +++ b/test/old/testdir/test_plugin_tar.vim @@ -266,3 +266,53 @@ func Test_extraction() bw! endfor endfunc + +func Test_extract_with_dotted_dir() + call delete('X.txt') + call writefile(['when they kiss they spit white noise'], 'X.txt') + + let dirname = tempname() + call mkdir(dirname, 'R') + let dirname = dirname .. '/foo.bar' + call mkdir(dirname, 'R') + let tarpath = dirname .. '/Xarchive.tar.gz' + call system('tar -czf ' .. tarpath .. ' X.txt') + call assert_true(filereadable(tarpath)) + call assert_equal(0, v:shell_error) + + call delete('X.txt') + defer delete(tarpath) + + execute 'e ' .. tarpath + call assert_match('X.txt', getline(5)) + :5 + normal x + call assert_true(filereadable('X.txt')) + call assert_equal(['when they kiss they spit white noise'], readfile('X.txt')) + call delete('X.txt') + bw! +endfunc + +func Test_extract_with_dotted_filename() + call delete('X.txt') + call writefile(['holiday inn'], 'X.txt') + + let dirname = tempname() + call mkdir(dirname, 'R') + let tarpath = dirname .. '/Xarchive.foo.tar.gz' + call system('tar -czf ' .. tarpath .. ' X.txt') + call assert_true(filereadable(tarpath)) + call assert_equal(0, v:shell_error) + + call delete('X.txt') + defer delete(tarpath) + + execute 'e ' .. tarpath + call assert_match('X.txt', getline(5)) + :5 + normal x + call assert_true(filereadable('X.txt')) + call assert_equal(['holiday inn'], readfile('X.txt')) + call delete('X.txt') + bw! +endfunc