runtime(zip): support PowerShell Core
fixes: #17987 closes: #18345 Signed-off-by: Shay <shay_public@hotmail.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
@ -16,6 +16,7 @@
|
|||||||
" 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows
|
" 2024 Aug 21 by Vim Project: simplify condition to detect MS-Windows
|
||||||
" 2025 Mar 11 by Vim Project: handle filenames with leading '-' correctly
|
" 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 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)
|
" License: Vim License (see vim's :help license)
|
||||||
" Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1
|
" Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1
|
||||||
" Permission is hereby granted to use and distribute this code,
|
" Permission is hereby granted to use and distribute this code,
|
||||||
@ -78,15 +79,124 @@ if v:version < 901
|
|||||||
finish
|
finish
|
||||||
endif
|
endif
|
||||||
" sanity checks
|
" 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")
|
call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
|
||||||
finish
|
finish
|
||||||
endif
|
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!")
|
call s:Mess('Error', "Warning: NOT executing " .. g:zip_unzipcmd .. " from current directory!")
|
||||||
finish
|
finish
|
||||||
endif
|
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
|
" Functions: {{{1
|
||||||
" ----------------
|
" ----------------
|
||||||
@ -105,7 +215,7 @@ fun! zip#Browse(zipfile)
|
|||||||
defer s:RestoreOpts(dict)
|
defer s:RestoreOpts(dict)
|
||||||
|
|
||||||
" sanity checks
|
" 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")
|
call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system")
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
@ -140,7 +250,10 @@ fun! zip#Browse(zipfile)
|
|||||||
\ '" Select a file with cursor and press ENTER'])
|
\ '" Select a file with cursor and press ENTER'])
|
||||||
keepj $
|
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
|
if v:shell_error != 0
|
||||||
call s:Mess('WarningMsg', "***warning*** (zip#Browse) ".fnameescape(a:zipfile)." is not a zip file")
|
call s:Mess('WarningMsg', "***warning*** (zip#Browse) ".fnameescape(a:zipfile)." is not a zip file")
|
||||||
keepj sil! %d
|
keepj sil! %d
|
||||||
@ -210,7 +323,7 @@ fun! zip#Read(fname,mode)
|
|||||||
endif
|
endif
|
||||||
let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\')
|
let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\')
|
||||||
" sanity check
|
" 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")
|
call s:Mess('Error', "***error*** (zip#Read) sorry, your system doesn't appear to have the ".g:zip_unzipcmd." program")
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
@ -220,7 +333,11 @@ fun! zip#Read(fname,mode)
|
|||||||
" but allows zipfile://... entries in quickfix lists
|
" but allows zipfile://... entries in quickfix lists
|
||||||
let temp = tempname()
|
let temp = tempname()
|
||||||
let fn = expand('%:p')
|
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 exe 'keepalt file '.temp
|
||||||
sil keepj e!
|
sil keepj e!
|
||||||
sil exe 'keepalt file '.fnameescape(fn)
|
sil exe 'keepalt file '.fnameescape(fn)
|
||||||
@ -241,7 +358,7 @@ fun! zip#Write(fname)
|
|||||||
defer s:RestoreOpts(dict)
|
defer s:RestoreOpts(dict)
|
||||||
|
|
||||||
" sanity checks
|
" 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")
|
call s:Mess('Error', "***error*** (zip#Write) sorry, your system doesn't appear to have the ".g:zip_zipcmd." program")
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
@ -273,7 +390,10 @@ fun! zip#Write(fname)
|
|||||||
let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','')
|
let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','')
|
||||||
endif
|
endif
|
||||||
if fname =~ '^[.]\{1,2}/'
|
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 fname = fname->substitute('^\([.]\{1,2}/\)\+', '', 'g')
|
||||||
let need_rename = 1
|
let need_rename = 1
|
||||||
endif
|
endif
|
||||||
@ -299,7 +419,20 @@ fun! zip#Write(fname)
|
|||||||
let fname = substitute(fname, '[', '[[]', 'g')
|
let fname = substitute(fname, '[', '[[]', 'g')
|
||||||
endif
|
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
|
if v:shell_error != 0
|
||||||
call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname)
|
call s:Mess('Error', "***error*** (zip#Write) sorry, unable to update ".zipfile." with ".fname)
|
||||||
|
|
||||||
@ -370,10 +503,14 @@ fun! zip#Extract()
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
" extract the file mentioned under the cursor
|
" 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
|
if v:shell_error != 0
|
||||||
call s:Mess('Error', "***error*** ".g:zip_extractcmd." ".b:zipfile." ".fname.": failed!")
|
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!")
|
call s:Mess('Error', "***error*** attempted to extract ".fname." but it doesn't appear to be present!")
|
||||||
else
|
else
|
||||||
echomsg "***note*** successfully extracted ".fname
|
echomsg "***note*** successfully extracted ".fname
|
||||||
|
|||||||
@ -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 |
|
| Zip File Interface |
|
||||||
@ -77,6 +77,16 @@ Copyright: Copyright (C) 2005-2015 Charles E Campbell *zip-copyright*
|
|||||||
"0": >
|
"0": >
|
||||||
let g:zip_exec=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~
|
PREVENTING LOADING~
|
||||||
|
|
||||||
If for some reason you do not wish to use vim to examine zipped files,
|
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
|
4. History *zip-history* {{{1
|
||||||
unreleased:
|
unreleased:
|
||||||
|
Sep 19, 2025 * support PowerShell Core
|
||||||
Jul 12, 2025 * drop ../ on write to prevent path traversal attacks
|
Jul 12, 2025 * drop ../ on write to prevent path traversal attacks
|
||||||
Mar 11, 2025 * handle filenames with leading '-' correctly
|
Mar 11, 2025 * handle filenames with leading '-' correctly
|
||||||
Aug 21, 2024 * simplify condition to detect MS-Windows
|
Aug 21, 2024 * simplify condition to detect MS-Windows
|
||||||
|
|||||||
Reference in New Issue
Block a user