diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-04 12:41:27 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-04 12:41:27 +0300 |
| commit | 4f2d36194b4f299aa7509d815c07121039ea833b (patch) | |
| tree | f3ded014bad3a4c76ff6a22b8726ebaab68c3d13 /mnv/runtime/autoload/zip.mnv | |
| parent | 5b578e70c314723a3cde5c9bfc2be0bf1dadc93b (diff) | |
| download | Project-Tick-4f2d36194b4f299aa7509d815c07121039ea833b.tar.gz Project-Tick-4f2d36194b4f299aa7509d815c07121039ea833b.zip | |
NOISSUE change uvim folder name to mnv
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'mnv/runtime/autoload/zip.mnv')
| -rw-r--r-- | mnv/runtime/autoload/zip.mnv | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/mnv/runtime/autoload/zip.mnv b/mnv/runtime/autoload/zip.mnv new file mode 100644 index 0000000000..e690c7a231 --- /dev/null +++ b/mnv/runtime/autoload/zip.mnv @@ -0,0 +1,590 @@ +" zip.mnv: Handles browsing zipfiles +" AUTOLOAD PORTION +" Date: 2024 Aug 21 +" Version: 34 +" Maintainer: This runtime file is looking for a new maintainer. +" Former Maintainer: Charles E Campbell +" Last Change: +" 2024 Jun 16 by MNV Project: handle whitespace on Windows properly (#14998) +" 2024 Jul 23 by MNV Project: fix 'x' command +" 2024 Jul 24 by MNV Project: use delete() function +" 2024 Jul 30 by MNV Project: fix opening remote zipfile +" 2024 Aug 04 by MNV Project: escape '[' in name of file to be extracted +" 2024 Aug 05 by MNV Project: workaround for the FreeBSD's unzip +" 2024 Aug 05 by MNV Project: clean-up and make it work with shellslash on Windows +" 2024 Aug 18 by MNV Project: correctly handle special globbing chars +" 2024 Aug 21 by MNV Project: simplify condition to detect MS-Windows +" 2025 Mar 11 by MNV Project: handle filenames with leading '-' correctly +" 2025 Jul 12 by MNV Project: drop ../ on write to prevent path traversal attacks +" 2025 Sep 22 by MNV Project: support PowerShell Core +" 2025 Dec 20 by MNV Project: use :lcd instead of :cd +" 2026 Feb 08 by MNV Project: use system() instead of :! +" 2026 Mar 08 by MNV Project: Make ZipUpdatePS() check for powershell +" 2026 Apr 01 by MNV Project: Detect more path traversal attacks +" License: MNV License (see mnv's :help license) +" Copyright: Copyright (C) 2005-2019 Charles E. Campbell {{{1 +" Permission is hereby granted to use and distribute this code, +" with or without modifications, provided that this copyright +" notice is copied with it. Like anything else that's free, +" zip.mnv and zipPlugin.mnv are provided *as is* and comes with +" no warranty of any kind, either expressed or implied. By using +" this plugin, you agree that in no event will the copyright +" holder be liable for any damages resulting from the use +" of this software. + +" --------------------------------------------------------------------- +" Load Once: {{{1 +if &cp || exists("g:loaded_zip") + finish +endif +let g:loaded_zip= "v34" +let s:keepcpo= &cpo +set cpo&mnv + +let s:zipfile_escape = ' ?&;\' +let s:ERROR = 2 +let s:WARNING = 1 +let s:NOTE = 0 + +" --------------------------------------------------------------------- +" Global Values: {{{1 +if !exists("g:zip_shq") + if &shq != "" + let g:zip_shq= &shq + elseif has("unix") + let g:zip_shq= "'" + else + let g:zip_shq= '"' + endif +endif +if !exists("g:zip_zipcmd") + let g:zip_zipcmd= "zip" +endif +if !exists("g:zip_unzipcmd") + let g:zip_unzipcmd= "unzip" +endif +if !exists("g:zip_extractcmd") + let g:zip_extractcmd= g:zip_unzipcmd +endif + +" --------------------------------------------------------------------- +" required early +" s:Mess: {{{2 +fun! s:Mess(group, msg) + redraw! + exe "echohl " . a:group + echomsg a:msg + echohl Normal +endfun + +if v:version < 901 + " required for defer + call s:Mess('WarningMsg', "***warning*** this version of zip needs mnv 9.1 or later") + finish +endif +" sanity checks +if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh' + call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system") + finish +endif +if !dist#mnv#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 &shell =~ 'pwsh' + 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 &shell =~ 'pwsh' && 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 +" ---------------- + +" --------------------------------------------------------------------- +" zip#Browse: {{{2 +fun! zip#Browse(zipfile) + " sanity check: ensure that the zipfile has "PK" as its first two letters + " (zip files have a leading PK as a "magic cookie") + if filereadable(a:zipfile) && readblob(a:zipfile, 0, 2) != 0z50.4B + exe "noswapfile noautocmd e " .. fnameescape(a:zipfile) + return + endif + + let dict = s:SetSaneOpts() + defer s:RestoreOpts(dict) + + " sanity checks + if !executable(g:zip_unzipcmd) && &shell !~ 'pwsh' + call s:Mess('Error', "***error*** (zip#Browse) unzip not available on your system") + return + endif + if !filereadable(a:zipfile) + if a:zipfile !~# '^\a\+://' + " if it's an url, don't complain, let url-handlers such as mnv do its thing + call s:Mess('Error', "***error*** (zip#Browse) File not readable <".a:zipfile.">") + endif + return + endif + if &ma != 1 + set ma + endif + let b:zipfile= a:zipfile + + setlocal noswapfile + setlocal buftype=nofile + setlocal bufhidden=hide + setlocal nobuflisted + setlocal nowrap + + " Oct 12, 2021: need to re-use Bram's syntax/tar.mnv. + " Setting the filetype to zip doesn't do anything (currently), + " but it is perhaps less confusing to curious perusers who do + " a :echo &ft + setf zip + run! syntax/tar.mnv + + " give header + call append(0, ['" zip.mnv version '.g:loaded_zip, + \ '" Browsing zipfile '.a:zipfile, + \ '" Select a file with cursor and press ENTER']) + keepj $ + + 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 + let eikeep= &ei + set ei=BufReadCmd,FileReadCmd + exe "keepj r ".fnameescape(a:zipfile) + let &ei= eikeep + keepj 1d + return + endif + + " Maps associated with zip plugin + setlocal noma nomod ro + noremap <silent> <buffer> <cr> :call <SID>ZipBrowseSelect()<cr> + noremap <silent> <buffer> x :call zip#Extract()<cr> + if &mouse != "" + noremap <silent> <buffer> <leftmouse> <leftmouse>:call <SID>ZipBrowseSelect()<cr> + endif + +endfun + +" --------------------------------------------------------------------- +" ZipBrowseSelect: {{{2 +fun! s:ZipBrowseSelect() + let dict = s:SetSaneOpts() + defer s:RestoreOpts(dict) + let fname= getline(".") + if !exists("b:zipfile") + return + endif + + " sanity check + if fname =~ '^"' + return + endif + if fname =~ '/$' + call s:Mess('Error', "***error*** (zip#Browse) Please specify a file, not a directory") + return + endif + + " get zipfile to the new-window + let zipfile = b:zipfile + let curfile = expand("%") + + noswapfile new + if !exists("g:zip_nomax") || g:zip_nomax == 0 + wincmd _ + endif + let s:zipfile_{winnr()}= curfile + exe "noswapfile e ".fnameescape("zipfile://".zipfile.'::'.fname) + filetype detect + +endfun + +" --------------------------------------------------------------------- +" zip#Read: {{{2 +fun! zip#Read(fname,mode) + let dict = s:SetSaneOpts() + defer s:RestoreOpts(dict) + + if has("unix") + let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') + else + let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') + endif + let fname = fname->substitute('[', '[[]', 'g')->escape('?*\\') + " sanity check + 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 + + " the following code does much the same thing as + " exe "keepj sil! r! ".g:zip_unzipcmd." -p -- ".s:Escape(zipfile,1)." ".s:Escape(fname,1) + " but allows zipfile://... entries in quickfix lists + let temp = tempname() + let fn = expand('%:p') + + let gnu_cmd = g:zip_unzipcmd . ' -p -- ' . s:Escape(zipfile, 0) . ' ' . s:Escape(fname, 0) . ' > ' . s:Escape(temp, 0) + let gnu_cmd = 'call system(''' . substitute(gnu_cmd, "'", "''", 'g') . ''')' + 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) + call delete(temp) + + filetype detect + + " cleanup + set nomod + +endfun + +" --------------------------------------------------------------------- +" zip#Write: {{{2 +fun! zip#Write(fname) + let dict = s:SetSaneOpts() + let need_rename = 0 + defer s:RestoreOpts(dict) + + " sanity checks + 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 + + if simplify(a:fname) =~ '\.\.[/\\]' + call s:Mess('Error', "***error*** (zip#Write) Path Traversal Attack detected, not writing!") + return + endif + + let curdir= getcwd() + let tmpdir= tempname() + if tmpdir =~ '\.' + let tmpdir= substitute(tmpdir,'\.[^.]*$','','e') + endif + call mkdir(tmpdir,"p") + + " attempt to change to the indicated directory + if s:ChgDir(tmpdir,s:ERROR,"(zip#Write) cannot lcd to temporary directory") + return + endif + + " place temporary files under .../_ZIPMNV_/ + if isdirectory("_ZIPMNV_") + call delete("_ZIPMNV_", "rf") + endif + call mkdir("_ZIPMNV_") + lcd _ZIPMNV_ + + if has("unix") + let zipfile = substitute(a:fname,'zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'zipfile://.\{-}::\([^\\].*\)$','\1','') + else + let zipfile = substitute(a:fname,'^.\{-}zipfile://\(.\{-}\)::[^\\].*$','\1','') + let fname = substitute(a:fname,'^.\{-}zipfile://.\{-}::\([^\\].*\)$','\1','') + endif + if fname =~ '^[.]\{1,2}/' + 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 + + if fname =~ '/' + let dirpath = substitute(fname,'/[^/]\+$','','e') + if has("win32unix") && executable("cygpath") + let dirpath = substitute(system("cygpath ".s:Escape(dirpath,0)),'\n','','e') + endif + call mkdir(dirpath,"p") + endif + if zipfile !~ '/' + let zipfile= curdir.'/'.zipfile + endif + + " don't overwrite files forcefully + exe "w ".fnameescape(fname) + if has("win32unix") && executable("cygpath") + let zipfile = substitute(system("cygpath ".s:Escape(zipfile,0)),'\n','','e') + endif + + if (has("win32") || has("win95") || has("win64") || has("win16")) && &shell !~? 'sh$' + let fname = substitute(fname, '[', '[[]', 'g') + endif + + 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' + " MNV 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) + + elseif s:zipfile_{winnr()} =~ '^\a\+://' + " support writing zipfiles across a network + let netzipfile= s:zipfile_{winnr()} + 1split|enew + let binkeep= &binary + let eikeep = &ei + set binary ei=all + exe "noswapfile e! ".fnameescape(zipfile) + call netrw#NetWrite(netzipfile) + let &ei = eikeep + let &binary = binkeep + q! + unlet s:zipfile_{winnr()} + elseif need_rename + exe $"sil keepalt file {fnameescape($"zipfile://{zipfile}::{fname}")}" + call s:Mess('Warning', "***error*** (zip#Browse) Path Traversal Attack detected, dropping relative path") + endif + + " cleanup and restore current directory + lcd .. + call delete("_ZIPMNV_", "rf") + call s:ChgDir(curdir,s:WARNING,"(zip#Write) unable to return to ".curdir."!") + call delete(tmpdir, "rf") + setlocal nomod +endfun + +" --------------------------------------------------------------------- +" zip#Extract: extract a file from a zip archive {{{2 +fun! zip#Extract() + + let dict = s:SetSaneOpts() + defer s:RestoreOpts(dict) + let fname= getline(".") + + " sanity check + if fname =~ '^"' + return + endif + if fname =~ '/$' + call s:Mess('Error', "***error*** (zip#Extract) Please specify a file, not a directory") + return + elseif fname =~ '^[.]\?[.]/' || simplify(fname) =~ '\.\.[/\\]' + call s:Mess('Error', "***error*** (zip#Browse) Path Traversal Attack detected, not extracting!") + return + endif + if filereadable(fname) + call s:Mess('Error', "***error*** (zip#Extract) <" .. fname .."> already exists in directory, not overwriting!") + return + endif + let target = fname->substitute('\[', '[[]', 'g') + " unzip 6.0 does not support -- to denote end-of-arguments + " unzip 6.1 (2010) apparently supports, it, but hasn't been released + " so the workaround is to use glob '[-]' so that it won't be considered an argument + " else, it would be possible to use 'unzip -o <file.zip> '-d/tmp' to extract the whole archive + let target = target->substitute('^-', '[&]', '') + if &shell =~ 'cmd' && has("win32") + let target = target + \ ->substitute('[?*]', '[&]', 'g') + \ ->substitute('[\\]', '?', 'g') + \ ->shellescape() + " there cannot be a file name with '\' in its name, unzip replaces it by _ + let fname = fname->substitute('[\\?*]', '_', 'g') + else + let target = target->escape('*?\\')->shellescape() + endif + + " extract the file mentioned under the cursor + 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) && &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 + endif +endfun + +" --------------------------------------------------------------------- +" s:Escape: {{{2 +fun! s:Escape(fname,isfilt) + if exists("*shellescape") + if a:isfilt + let qnameq= shellescape(a:fname,1) + else + let qnameq= shellescape(a:fname) + endif + else + let qnameq= g:zip_shq.escape(a:fname,g:zip_shq).g:zip_shq + endif + return qnameq +endfun + +" --------------------------------------------------------------------- +" s:ChgDir: {{{2 +fun! s:ChgDir(newdir,errlvl,errmsg) + try + exe "lcd ".fnameescape(a:newdir) + catch /^MNV\%((\a\+)\)\=:E344/ + redraw! + if a:errlvl == s:NOTE + echomsg "***note*** ".a:errmsg + elseif a:errlvl == s:WARNING + call s:Mess("WarningMsg", "***warning*** ".a:errmsg) + elseif a:errlvl == s:ERROR + call s:Mess("Error", "***error*** ".a:errmsg) + endif + return 1 + endtry + + return 0 +endfun + +" --------------------------------------------------------------------- +" s:SetSaneOpts: {{{2 +fun! s:SetSaneOpts() + let dict = {} + let dict.report = &report + let dict.shellslash = &shellslash + + let &report = 10 + let &shellslash = 0 + + return dict +endfun + +" --------------------------------------------------------------------- +" s:RestoreOpts: {{{2 +fun! s:RestoreOpts(dict) + for [key, val] in items(a:dict) + exe $"let &{key} = {val}" + endfor +endfun + +" ------------------------------------------------------------------------ +" Modelines And Restoration: {{{1 +let &cpo= s:keepcpo +unlet s:keepcpo +" mnv:ts=8 fdm=marker |
