patch 9.2.0325: runtime(tar): bug in zstd handling

Commit: 
https://github.com/vim/vim/commit/00285c035aa7d92971bc50b7b124e1408dda53ad
Author: Aaron Burrow <[email protected]>
Date:   Thu Apr 9 19:06:13 2026 +0000

    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: #19930
    
    Signed-off-by: Aaron Burrow <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim
index 110327e95..b068bd181 100644
--- a/runtime/autoload/tar.vim
+++ b/runtime/autoload/tar.vim
@@ -21,6 +21,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 <tar.vim>
 "
@@ -687,7 +688,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!")
@@ -696,7 +697,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/src/testdir/test_plugin_tar.vim b/src/testdir/test_plugin_tar.vim
index d6dbdd761..28cf709f8 100644
--- a/src/testdir/test_plugin_tar.vim
+++ b/src/testdir/test_plugin_tar.vim
@@ -148,56 +148,118 @@ def g:Test_tar_path_traversal_with_nowrapscan()
   bw!
 enddef
 
-def g:Test_tar_lz4_extract()
-  CheckExecutable lz4
-
-  delete('X.txt')
-  delete('Xarchive.tar')
-  delete('Xarchive.tar.lz4')
-  call writefile(['hello'], 'X.txt')
-  call system('tar -cf Xarchive.tar X.txt')
+def CreateTar(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar -C ' .. tempdir .. ' -cf ' .. outputdir .. '/' .. 
archivename .. ' X.txt')
   assert_equal(0, v:shell_error)
+enddef
 
-  call system('lz4 -z Xarchive.tar Xarchive.tar.lz4')
+def CreateTgz(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar -C ' .. tempdir .. ' -czf ' .. outputdir .. '/' .. 
archivename .. ' X.txt')
   assert_equal(0, v:shell_error)
+enddef
 
-  delete('X.txt')
-  delete('Xarchive.tar')
-  defer delete('Xarchive.tar.lz4')
-
-  e Xarchive.tar.lz4
-  assert_match('X.txt', getline(5))
-  :5
-  normal x
-  assert_true(filereadable('X.txt'))
-  assert_equal(['hello'], readfile('X.txt'))
-  delete('X.txt')
-  bw!
+def CreateTbz(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar -C ' .. tempdir .. ' -cjf ' .. outputdir .. '/' .. 
archivename .. ' X.txt')
+  assert_equal(0, v:shell_error)
 enddef
 
-def g:Test_tlz4_extract()
-  CheckExecutable lz4
+def CreateTxz(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar -C ' .. tempdir .. ' -cJf ' .. outputdir .. '/' .. 
archivename .. ' X.txt')
+  assert_equal(0, v:shell_error)
+enddef
 
-  delete('X.txt')
-  delete('Xarchive.tar')
-  delete('Xarchive.tlz4')
-  call writefile(['goodbye'], 'X.txt')
-  call system('tar -cf Xarchive.tar X.txt')
+def CreateTzst(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar --zstd -C ' .. tempdir .. ' -cf ' .. outputdir .. '/' .. 
archivename .. ' X.txt')
   assert_equal(0, v:shell_error)
+enddef
 
-  call system('lz4 -z Xarchive.tar Xarchive.tlz4')
+def CreateTlz4(archivename: string, content: string, outputdir: string)
+  var tempdir = tempname()
+  mkdir(tempdir, 'R')
+  call writefile([content], tempdir .. '/X.txt')
+  assert_true(filereadable(tempdir .. '/X.txt'))
+  call system('tar -C ' .. tempdir .. ' -cf ' .. tempdir .. '/Xarchive.tar 
X.txt')
+  assert_equal(0, v:shell_error)
+  assert_true(filereadable(tempdir .. '/Xarchive.tar'))
+  call system('lz4 -z ' .. tempdir .. '/Xarchive.tar ' .. outputdir .. '/' .. 
archivename)
   assert_equal(0, v:shell_error)
+enddef
 
-  delete('X.txt')
-  delete('Xarchive.tar')
-  defer delete('Xarchive.tlz4')
+# XXX: Add test for .tar.bz3
+def g:Test_extraction()
+  var control = [
+    {create: CreateTar,
+     archive: 'Xarchive.tar'},
+    {create: CreateTgz,
+     archive: 'Xarchive.tgz'},
+    {create: CreateTgz,
+     archive: 'Xarchive.tar.gz'},
+    {create: CreateTbz,
+     archive: 'Xarchive.tbz'},
+    {create: CreateTbz,
+     archive: 'Xarchive.tar.bz2'},
+    {create: CreateTxz,
+     archive: 'Xarchive.txz'},
+    {create: CreateTxz,
+     archive: 'Xarchive.tar.xz'},
+  ]
+
+  if executable('lz4') == 1
+    control->add({
+      create: CreateTlz4,
+      archive: 'Xarchive.tar.lz4'
+    })
+    control->add({
+      create: CreateTlz4,
+      archive: 'Xarchive.tlz4'
+    })
+  endif
+  if executable('zstd') == 1
+    control->add({
+      create: CreateTzst,
+      archive: 'Xarchive.tar.zst'
+    })
+    control->add({
+      create: CreateTzst,
+      archive: 'Xarchive.tzst'
+    })
+  endif
 
-  e Xarchive.tlz4
-  assert_match('X.txt', getline(5))
-  :5
-  normal x
-  assert_true(filereadable('X.txt'))
-  assert_equal(['goodbye'], readfile('X.txt'))
-  delete('X.txt')
-  bw!
+  for c in control
+    var dir = tempname()
+    mkdir(dir, 'R')
+    call(c.create, [c.archive, 'hello', dir])
+
+    delete('X.txt')
+    execute 'edit ' .. dir .. '/' .. c.archive
+    assert_match('X.txt', getline(5), 'line 5 wrong in archive: ' .. c.archive)
+    :5
+    normal x
+    assert_equal(0, v:shell_error, 'vshell error not 0')
+    assert_true(filereadable('X.txt'), 'X.txt not readable for archive: ' .. 
c.archive)
+    assert_equal(['hello'], readfile('X.txt'), 'X.txt wrong contents for 
archive: ' .. c.archive)
+    delete('X.txt')
+    delete(dir .. '/' .. c.archive)
+    bw!
+  endfor
 enddef
diff --git a/src/version.c b/src/version.c
index 99a25eb7c..5fc6428c2 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    325,
 /**/
     324,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wAv4m-00C7Ah-48%40256bit.org.

Raspunde prin e-mail lui