runtime(zip): support PowerShell Core

Commit: 
https://github.com/vim/vim/commit/70d745a61bc12d94b9b217887004c1a5c263cb9d
Author: Shay <[email protected]>
Date:   Mon Sep 22 19:02:24 2025 +0000

    runtime(zip): support PowerShell Core
    
    fixes: https://github.com/vim/vim/issues/17987
    closes: https://github.com/vim/vim/issues/18345
    
    Signed-off-by: Shay <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/autoload/zip.vim b/runtime/autoload/zip.vim
index c46ec4470..49e4e8198 100644
--- a/runtime/autoload/zip.vim
+++ b/runtime/autoload/zip.vim
@@ -1,4 +1,4 @@
-" zip.vim: Handles browsing zipfiles
+  " zip.vim: Handles browsing zipfiles
 " AUTOLOAD PORTION
 " Date:                2024 Aug 21
 " Version:     34
@@ -16,6 +16,7 @@
 " 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows
 " 2025 Mar 11 by Vim Project: handle filenames with leading '-' correctly
 " 2025 Jul 12 by Vim Project: drop ../ on write to prevent path traversal 
attacks
+" 2025 Sep 22 by Vim Project: support PowerShell Core
 " License:     Vim License  (see vim's :help license)
 " Copyright:   Copyright (C) 2005-2019 Charles E. Campbell {{{1
 "              Permission is hereby granted to use and distribute this code,
@@ -78,15 +79,124 @@ if v:version < 901
  finish
 endif
 " sanity checks
-if !executable(g:zip_unzipcmd)
+if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
  call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your 
system")
  finish
 endif
-if !dist#vim#IsSafeExecutable('zip', g:zip_unzipcmd)
+if !dist#vim#IsSafeExecutable('zip', g:zip_unzipcmd) && &shell !~ 'pwsh'
  call s:Mess('Error', "Warning: NOT executing " .. g:zip_unzipcmd .. " from 
current directory!")
  finish
 endif
 
+" ----------------
+"  PowerShell: {{{1
+" ----------------
+
+function! s:TryExecGnuFallBackToPs(executable, gnu_func_call, ...)
+  " Check that a gnu executable is available, run the gnu_func_call if so. If
+  " the gnu executable is not available or if gnu_func_call fails, try
+  " ps_func_call if &shell =~ 'pwsh'. If all attempts fail, print errors.
+  " a:executable - one of (g:zip_zipcmd, g:zip_unzipcmd, g:zip_extractcmd)
+  " a:gnu_func_call - (string) a gnu function call to execute
+  " a:1 - (optional string) a PowerShell function call to execute.
+  let failures = []
+  if executable(substitute(a:executable,'\s\+.*$','',''))
+    try
+      exe a:gnu_func_call
+      return
+    catch
+      call add(failures, 'Failed to execute '.a:gnu_func_call)
+    endtry
+  else
+    call add(failures, a:executable.' not available on your system')
+  endif
+  if &shell =~ 'pwsh' && a:0 == 1
+    try
+      exe a:1
+      return
+    catch
+      call add(failures, 'Fallback to PowerShell attempted but failed')
+    endtry
+  endif
+  for msg in failures
+    call s:Mess('Error', msg)
+  endfor
+endfunction
+
+
+function! s:ZipBrowsePS(zipfile)
+  " Browse the contents of a zip file using PowerShell's
+  " Equivalent `unzip -Z1 -- zipfile`
+  let cmds = [
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
+        \ '$zip.Entries | ForEach-Object { $_.FullName };',
+        \ '$zip.Dispose()'
+        \ ]
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+endfunction
+
+function! s:ZipReadPS(zipfile, fname, tempfile)
+  " Read a filename within a zipped file to a temporary file.
+  " Equivalent to `unzip -p -- zipfile fname > tempfile`
+  if a:fname =~ '/'
+    call s:Mess('WarningMsg', "***warning*** PowerShell can display, but 
cannot update, files in archive subfolders")
+  endif
+  let cmds = [
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
+        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
s:Escape(a:fname, 1) . ' };',
+        \ '$stream = $fileEntry.Open();',
+        \ '$fileStream = [System.IO.File]::Create(' . s:Escape(a:tempfile, 1) 
. ');',
+        \ '$stream.CopyTo($fileStream);',
+        \ '$fileStream.Close();',
+        \ '$stream.Close();',
+        \ '$zip.Dispose()'
+        \ ]
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+endfunction
+
+function! s:ZipUpdatePS(zipfile, fname)
+  " Update a filename within a zipped file
+  " Equivalent to `zip -u zipfile fname`
+  if a:fname =~ '/'
+    call s:Mess('Error', "***error*** PowerShell cannot update files in 
archive subfolders")
+    return ':'
+  endif
+  return 'Compress-Archive -Path ' . a:fname . ' -Update -DestinationPath ' . 
a:zipfile
+endfunction
+
+function! s:ZipExtractFilePS(zipfile, fname)
+  " Extract a single file from an archive
+  " Equivalent to `unzip -o zipfile fname`
+  if a:fname =~ '/'
+    call s:Mess('Error', "***error*** PowerShell cannot extract files in 
archive subfolders")
+    return ':'
+  endif
+  let cmds = [
+        \ '$zip = [System.IO.Compression.ZipFile]::OpenRead(' . 
s:Escape(a:zipfile, 1) . ');',
+        \ '$fileEntry = $zip.Entries | Where-Object { $_.FullName -eq ' . 
a:fname . ' };',
+        \ '$stream = $fileEntry.Open();',
+        \ '$fileStream = [System.IO.File]::Create(' . a:fname . ');',
+        \ '$stream.CopyTo($fileStream);',
+        \ '$fileStream.Close();',
+        \ '$stream.Close();',
+        \ '$zip.Dispose()'
+        \ ]
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+endfunction
+
+function! s:ZipDeleteFilePS(zipfile, fname)
+  " Delete a single file from an archive
+  " Equivalent to `zip -d zipfile fname`
+  let cmds = [
+        \ 'Add-Type -AssemblyName System.IO.Compression.FileSystem;',
+        \ '$zip = [System.IO.Compression.ZipFile]::Open(' . 
s:Escape(a:zipfile, 1) . ', ''Update'');',
+        \ '$entry = $zip.Entries | Where-Object { $_.Name -eq ' . 
s:Escape(a:fname, 1) . ' };',
+        \ 'if ($entry) { $entry.Delete(); $zip.Dispose() }',
+        \ 'else { $zip.Dispose() }'
+        \ ]
+  return 'pwsh -NoProfile -Command ' . s:Escape(join(cmds, ' '), 1)
+endfunction
+
 " ----------------
 "  Functions: {{{1
 " ----------------
@@ -105,7 +215,7 @@ fun! zip#Browse(zipfile)
   defer s:RestoreOpts(dict)
 
   " sanity checks
-  if !executable(g:zip_unzipcmd)
+  if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh'
    call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your 
system")
    return
   endif
@@ -140,7 +250,10 @@ fun! zip#Browse(zipfile)
  \                '" Select a file with cursor and press ENTER'])
   keepj $
 
-  exe $"keepj sil r! {g:zip_unzipcmd} -Z1 -- {s:Escape(a:zipfile, 1)}"
+  let gnu_cmd = "keepj sil r! " . g:zip_unzipcmd . " -Z1 -- " . 
s:Escape(a:zipfile, 1)
+  let ps_cmd = 'keepj sil r! ' . s:ZipBrowsePS(a:zipfile)
+  call s:TryExecGnuFallBackToPs(g:zip_unzipcmd, gnu_cmd, ps_cmd)
+
   if v:shell_error != 0
    call s:Mess('WarningMsg', "***warning*** (zip#Browse) 
".fnameescape(a:zipfile)." is not a zip file")
    keepj sil! %d
@@ -210,7 +323,7 @@ fun! zip#Read(fname,mode)
   endif
   let fname    = fname->substitute('[', '[[]', 'g')->escape('?*\')
   " sanity check
-  if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','',''))
+  if !executable(substitute(g:zip_unzipcmd,'\s\+.*$','',''))  && &shell !~ 
'pwsh'
    call s:Mess('Error', "***error*** (zip#Read) sorry, your system doesn't 
appear to have the ".g:zip_unzipcmd." program")
    return
   endif
@@ -220,7 +333,11 @@ fun! zip#Read(fname,mode)
   " but allows zipfile://... entries in quickfix lists
   let temp = tempname()
   let fn   = expand('%:p')
-  exe "sil !".g:zip_unzipcmd." -p -- ".s:Escape(zipfile,1)." 
".s:Escape(fname,1).' > '.temp
+
+  let gnu_cmd = 'sil !' . g:zip_unzipcmd . ' -p -- ' . s:Escape(zipfile, 1) . 
' ' . s:Escape(fname, 1) . ' > ' . s:Escape(temp, 1)
+  let ps_cmd = 'sil !' . s:ZipReadPS(zipfile, fname, temp)
+  call s:TryExecGnuFallBackToPs(g:zip_unzipcmd, gnu_cmd, ps_cmd)
+
   sil exe 'keepalt file '.temp
   sil keepj e!
   sil exe 'keepalt file '.fnameescape(fn)
@@ -241,7 +358,7 @@ fun! zip#Write(fname)
   defer s:RestoreOpts(dict)
 
   " sanity checks
-  if !executable(substitute(g:zip_zipcmd,'\s\+.*$','',''))
+  if !executable(substitute(g:zip_zipcmd,'\s\+.*$','','')) && &shell !~ 'pwsh'
     call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't 
appear to have the ".g:zip_zipcmd." program")
     return
   endif
@@ -273,7 +390,10 @@ fun! zip#Write(fname)
     let fname   = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\].*\)$',' 
','')
   endif
   if fname =~ '^[.]\{1,2}/'
-    call system(g:zip_zipcmd." -d ".s:Escape(fnamemodify(zipfile,":p"),0)." 
".s:Escape(fname,0))
+    let gnu_cmd = g:zip_zipcmd . ' -d ' . 
s:Escape(fnamemodify(zipfile,":p"),0) . ' ' . s:Escape(fname,0) 
+    let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . 
''')'
+    let ps_cmd = $"call system({s:Escape(s:ZipDeleteFilePS(zipfile, fname), 
1)})"
+    call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
     let fname = fname->substitute('^\([.]\{1,2}/\)\+', '', 'g')
     let need_rename = 1
   endif
@@ -299,7 +419,20 @@ fun! zip#Write(fname)
     let fname = substitute(fname, '[', '[[]', 'g')
   endif
 
-  call system(g:zip_zipcmd." -u ".s:Escape(fnamemodify(zipfile,":p"),0)." 
".s:Escape(fname,0))
+  let gnu_cmd = g:zip_zipcmd . ' -u '. s:Escape(fnamemodify(zipfile,":p"),0) . 
' ' . s:Escape(fname,0) 
+  let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')'
+  let ps_cmd = s:ZipUpdatePS(s:Escape(fnamemodify(zipfile, ':p'), 0), 
s:Escape(fname, 0))
+  let ps_cmd = 'call system(''' . substitute(ps_cmd, "'", "''", 'g') . ''')'
+  call s:TryExecGnuFallBackToPs(g:zip_zipcmd, gnu_cmd, ps_cmd)
+  if &shell =~ 'pwsh'
+    " Vim flashes 'creation in progress ...' from what I believe is the
+    " ProgressAction stream of PowerShell. Unfortunately, this cannot be
+    " suppressed (as of 250824) due to an open PowerShell issue.
+    " https://github.com/PowerShell/PowerShell/issues/21074
+    " This necessitates a redraw of the buffer.
+    redraw!
+  endif
+
   if v:shell_error != 0
     call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update 
".zipfile." with ".fname)
 
@@ -370,10 +503,14 @@ fun! zip#Extract()
   endif
 
   " extract the file mentioned under the cursor
-  call system($"{g:zip_extractcmd} -o {shellescape(b:zipfile)} {target}")
+  let gnu_cmd = g:zip_extractcmd . ' -o '. shellescape(b:zipfile) . ' ' . 
target
+  let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')'
+  let ps_cmd = $"call system({s:Escape(s:ZipExtractFilePS(b:zipfile, target), 
1)})"
+  call s:TryExecGnuFallBackToPs(g:zip_extractcmd, gnu_cmd, ps_cmd)
+
   if v:shell_error != 0
     call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." 
".fname.": failed!")
-  elseif !filereadable(fname)
+  elseif !filereadable(fname) && &shell !~ 'pwsh'
     call s:Mess('Error', "***error*** attempted to extract ".fname." but it 
doesn't appear to be present!")
   else
     echomsg "***note*** successfully extracted ".fname
diff --git a/runtime/doc/pi_zip.txt b/runtime/doc/pi_zip.txt
index 0f7ef4ec3..b9ee44b7e 100644
--- a/runtime/doc/pi_zip.txt
+++ b/runtime/doc/pi_zip.txt
@@ -1,4 +1,4 @@
-*pi_zip.txt*   For Vim version 9.1.  Last change: 2025 Jul 15
+*pi_zip.txt*   For Vim version 9.1.  Last change: 2025 Sep 22
 
                                +====================+
                                | Zip File Interface |
@@ -50,7 +50,7 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell  
*zip-copyright*
    allow spaces and whatnot in filenames; however, if it is incorrectly
    guessing the quote to use for your setup, you may use >
        g:zip_shq
-<  which by default is a single quote under Unix (') and a double quote
+<   which by default is a single quote under Unix (') and a double quote
    under Windows (").  If you'd rather have no quotes, simply set
    g:zip_shq to the empty string (let g:zip_shq= "") in your <.vimrc>.
 
@@ -77,6 +77,16 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell        
 *zip-copyright*
    "0": >
        let g:zip_exec=0
 <
+   FALLBACK TO POWERSHELL CORE~
+
+   This plugin will first attempt to use the (more capable) GNU zip/unzip
+   commands.  If these commands are not available or fail, and the user is
+   using PowerShell Core (i.e., the 'shell' option matches "pwsh"), the
+   plugin will fall back to a PowerShell Core cmdlet.  The PowerShell Core
+   cmdlets are limited: they cannot write or extract files within
+   subdirectories of a zip archive.  The advantage, however, is that no
+   separate unzip binary needs to be installed.
+
    PREVENTING LOADING~
 
    If for some reason you do not wish to use vim to examine zipped files,
@@ -112,6 +122,7 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell       
 *zip-copyright*
 ==============================================================================
 4. History                                                     *zip-history* 
{{{1
    unreleased:
+       Sep 19, 2025 * support PowerShell Core
        Jul 12, 2025 * drop ../ on write to prevent path traversal attacks
        Mar 11, 2025 * handle filenames with leading '-' correctly
        Aug 21, 2024 * simplify condition to detect MS-Windows

-- 
-- 
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/E1v0m0C-0059yj-OT%40256bit.org.

Raspunde prin e-mail lui